From ad4f26cdd4aefa9f8504ba36b84cbade0ee4e39f Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:41:13 -0400 Subject: [PATCH 01/59] feat(build): build, verify and package the mobile web bundle with every desktop release (OTA phase A, 2/5) (#21326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mobile-web): add the Phase A bootstrap web source A peer of src/ so the root workspace owns it and mobile's separate lockfile stays out of packaging. Four assets across four content types, enough to exercise multi-asset manifest handling rather than assume it. The page reads buildId from manifest.json at runtime: buildId hashes the asset list that index.html belongs to, so injecting it into a hashed asset would make that asset's hash depend on itself. Registered as a fourth typecheck project; without it the entry would be the only TypeScript in a release path that tsc never sees. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(build): build and verify the mobile web bundle from the root workspace Root esbuild over mobile-web/ into out/mobile-web/, content-addressed as assets/. with index.html the only stable name. buildId is the sha256 of the canonical serialization of the sorted asset list, so it is a pure function of content and usable as a cache key with no further reasoning. The verifier builds twice into scratch dirs and compares: a timestamp, an absolute path, or an unstable ordering fails the build when someone introduces it, not the first time a phone gets a spurious cache miss. It also enforces the Phase A budget of 16 assets and 256 KiB, separate from the permanent contract ceiling. build:release does not call build:desktop, so build:mobile-web is wired into build:desktop, build:release, and build:release:parallel. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(packaging): fail the release when the mobile web bundle is missing or stale electron-builder only warns about a missing input, so without a beforePack guard a release ships 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/. The source tree is excluded from app.asar; out/mobile-web ships inside it under the existing out rules, exactly as out/web does. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-web): narrow the manifest with `in` instead of a cast The changed-code casting gate rejects assertions, and `in` narrows the same untrusted JSON without one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web): move the bundle source under src/ so the root guard passes .github/scripts/check-root-directory-entries.mjs blocks any new top-level entry by name, so mobile-web/ could not live at the root. The source is excluded from app.asar by the existing '!src{,/**/*}' rule; the explicit '!src/mobile-web{,/**/*}' entry stays as a marker. out/mobile-web is unaffected and still ships under the out rules like out/web. No tsconfig includes src/**, so node, web, cli, and relay do not pick the tree up; it is registered as a knip entry so audit:dead-code does not call it unused. buildId is unchanged at 9d78435e: the builder hashes content, not paths. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(build): resolve the entry-script guard through pathToFileURL `file://${process.argv[1]}` never equals import.meta.url on Windows, where that url is file:///C:/... So the builder exited 0 having written nothing and the Windows packaging job failed later, at the guard, with no clue why. Every other script in config/scripts already uses pathToFileURL; this one now does too, via an exported predicate a posix runner can exercise with a win32 path. The verify script had no entry guard at all, so importing its budget constants ran the whole verification — including its process.exit — inside the test worker. It is now a function behind the same guard. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(ci): build the mobile web bundle in the PR package job That job assembles packaging inputs step by step instead of calling build:release, so the new beforePack guard hard-failed it. The census test added here is the oracle: it walks every workflow job that invokes electron-builder without --prepackaged (which short-circuits doPack before beforePack) and requires a bundle-producing script in the same job. It goes red on exactly pr.yml's package job when this step is removed. Ten jobs covered; the other nine already ran build:release, build:release:parallel, or build:desktop. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web): pin source line endings, because CRLF changes the buildId Every text byte under src/mobile-web 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 instead of 9d78435e. That would make a Windows-built desktop disagree with a mac-built one about which bundle a phone has cached. .gitattributes pins eol=lf for the text sources and -text for the PNG, matching the four trees already pinned for byte-hashing. The verify script asserts no source file carries a CR, so the build fails if the pin ever stops applying rather than silently shipping a second bundle identity. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(build): read the test's own path from import.meta.filename oxlint unicorn/prefer-import-meta-properties. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(test): census packaging jobs over raw workflow text, not re-serialized YAML yaml.stringify folds long lines, and in dev-channel-win-build.yml's build-win the fold landed between `electron-builder` and `--config`, so a real packaging job was invisible to the census: 11 jobs exist, the test saw 10. Slice each job's raw source by its parsed boundaries instead, and pin the inventory so a new packaging workflow has to be added here on purpose. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(build): assert the script chain the packaging census trusts The census only checks that a packaging job invokes one of ten build scripts; that those scripts still reach build:mobile-web was asserted nowhere, so a dropped link would leave every job looking covered while packaging failed at beforePack. Resolve each script for real, and pin pr.yml's hand-rolled step, since that job never calls build:release. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(build): realpath the entry path before the direct-invocation compare Node resolves symlinks in import.meta.url but not in argv[1], so `node /tmp/...` against a /private/tmp realpath compared two different strings: the builder and the verifier exited 0 having written and checked nothing. Same silent-success shape as the Windows file:// bug, so the fix sits next to it, with both seams injectable. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile-web): format bootstrap.css with oxfmt It was the only tracked CSS failing oxfmt --check. The buildId is unchanged at 9d78435e8bb73c3341f833c20aaefbd7bfdfc414b68dadf87c1689d86728fe33, because esbuild's CSS minifier normalises the whitespace this touches before the asset is hashed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(packaging): reject bundle files the manifest does not list The guard only walked the manifest, so a dropped assets/stale.js passed: assets are content-addressed, nothing ever overwrites a stale copy, and it would ship inside asar unreachable and unverified. Require every file under out/mobile-web to be the manifest or a listed asset. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(packaging): give beforePack an explicit mobile web bundle root The bundle guard read the repo's out/mobile-web unconditionally, so the two arch-aware packaging tests that call the real beforePack went red in the unit-test job, which never runs build:mobile-web. beforePack now takes the bundle root as a second parameter defaulting to out/mobile-web, which is what electron-builder gets, and those tests build a real bundle into a temp dir instead. The guard is neither skipped nor made tolerant of a missing bundle. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(packaging): census sees script-wrapped packers; dev verify reuses the guard The workflow census only matched a literal `electron-builder --config` line, so daemon-relocation-spike's `pnpm run build:unpack` (which packs and runs beforePack) was invisible to it. Jobs now count when any `pnpm run + + diff --git a/src/mobile-web/src/bootstrap.css b/src/mobile-web/src/bootstrap.css new file mode 100644 index 00000000000..3fc2529db97 --- /dev/null +++ b/src/mobile-web/src/bootstrap.css @@ -0,0 +1,54 @@ +:root { + color-scheme: dark light; + --bootstrap-fg: #e6edf3; + --bootstrap-muted: #8b98a5; + --bootstrap-bg: #0d1117; +} + +body { + margin: 0; + background: var(--bootstrap-bg); + color: var(--bootstrap-fg); + font-family: + ui-sans-serif, + system-ui, + -apple-system, + 'Segoe UI', + sans-serif; +} + +.bootstrap { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + padding: 24px; +} + +.bootstrap__mark { + image-rendering: pixelated; +} + +.bootstrap__title { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.bootstrap__facts { + display: grid; + grid-template-columns: max-content 1fr; + gap: 4px 12px; + margin: 0; + font-size: 13px; +} + +.bootstrap__facts dt { + color: var(--bootstrap-muted); +} + +.bootstrap__facts dd { + margin: 0; + overflow-wrap: anywhere; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} diff --git a/src/mobile-web/src/bootstrap.ts b/src/mobile-web/src/bootstrap.ts new file mode 100644 index 00000000000..805ee5381df --- /dev/null +++ b/src/mobile-web/src/bootstrap.ts @@ -0,0 +1,65 @@ +// Build-time constants, substituted by config/scripts/build-mobile-web-bundle.mjs via esbuild define. +declare const ORCA_MOBILE_WEB_DESKTOP_VERSION: string +declare const ORCA_MOBILE_WEB_RUNTIME_PROTOCOL_VERSION: number +declare const ORCA_MOBILE_WEB_MIN_COMPATIBLE_RUNTIME_PROTOCOL_VERSION: number + +// Why a runtime read and not a define: buildId is the hash of the asset list that index.html +// belongs to, so injecting it into a hashed asset would make the hash depend on itself. +const MANIFEST_URL = './manifest.json' + +function isBuildId(value: unknown): value is string { + return typeof value === 'string' && /^[0-9a-f]{64}$/.test(value) +} + +async function readBuildId(): Promise { + const response = await fetch(MANIFEST_URL, { cache: 'no-store' }) + if (!response.ok) { + throw new Error(`manifest request failed with ${String(response.status)}`) + } + const manifest: unknown = await response.json() + // `in` narrows without an assertion; the manifest is untrusted JSON either way. + if (typeof manifest !== 'object' || manifest === null || !('buildId' in manifest)) { + throw new Error('manifest has no buildId') + } + const { buildId } = manifest + if (!isBuildId(buildId)) { + throw new Error('manifest buildId is not a sha256 digest') + } + return buildId +} + +function renderFacts(facts: readonly (readonly [string, string])[]): void { + const list = document.getElementById('bootstrap-facts') + if (!(list instanceof HTMLDListElement)) { + return + } + list.replaceChildren() + for (const [term, description] of facts) { + const dt = document.createElement('dt') + dt.textContent = term + const dd = document.createElement('dd') + dd.textContent = description + dd.dataset.fact = term + list.append(dt, dd) + } +} + +async function start(): Promise { + let buildId: string + try { + buildId = await readBuildId() + } catch (error) { + buildId = `unavailable (${error instanceof Error ? error.message : String(error)})` + } + renderFacts([ + ['buildId', buildId], + ['desktopVersion', ORCA_MOBILE_WEB_DESKTOP_VERSION], + ['runtimeProtocolVersion', String(ORCA_MOBILE_WEB_RUNTIME_PROTOCOL_VERSION)], + [ + 'minCompatibleRuntimeProtocolVersion', + String(ORCA_MOBILE_WEB_MIN_COMPATIBLE_RUNTIME_PROTOCOL_VERSION) + ] + ]) +} + +void start() diff --git a/src/mobile-web/src/orca-mark.png b/src/mobile-web/src/orca-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..274fd7bf7824726a834ca751c72d4386b7567a06 GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`2A(dCAr-fh6C~;y>fdepuVlR; zd4i1N*&_*a*bd|ze9k0bo5i5ZyTC$9)6c_Mn1P{d*3*Y7Hhb;>^)h(6`njxgN@xNA DvG5 Date: Thu, 17 Sep 2026 22:52:49 -0400 Subject: [PATCH 02/59] fix: make worktree scan failures actionable (#21291) * fix: make worktree scan failures actionable * fix: preserve remote worktree scan diagnostics --- .../listing/detected-provider-listing.ts | 8 +- .../rows/RepoScanUnavailableIndicator.tsx | 154 +++++++++++++----- src/renderer/src/i18n/locales/en.json | 7 +- .../listing/detected-worktree-host-merge.ts | 1 + .../detected-worktree-provider-request.ts | 4 +- .../listing/worktree-catalog-visibility.ts | 1 + src/shared/worktree-scan-failure.test.ts | 28 ++++ src/shared/worktree-scan-failure.ts | 25 +++ src/shared/worktree/types.ts | 3 + 9 files changed, 189 insertions(+), 42 deletions(-) create mode 100644 src/shared/worktree-scan-failure.test.ts create mode 100644 src/shared/worktree-scan-failure.ts diff --git a/src/main/ipc/worktrees/listing/detected-provider-listing.ts b/src/main/ipc/worktrees/listing/detected-provider-listing.ts index ec5e7606e45..4feeb42dd58 100644 --- a/src/main/ipc/worktrees/listing/detected-provider-listing.ts +++ b/src/main/ipc/worktrees/listing/detected-provider-listing.ts @@ -31,6 +31,7 @@ import { warnOnce } from './worktree-listing-diagnostics' import { readAllWorktreeMetaForRepo } from '../../../persistence/host-qualified-worktree-meta' +import { classifyWorktreeScanFailure } from '../../../../shared/worktree-scan-failure' export async function listDetectedWorktreesForCapturedRepo( store: Store, @@ -163,6 +164,7 @@ export async function listDetectedWorktreesForCapturedRepo( ) // Why: retention alone leaves inert rows with no explanation; the cause rides with the listing. const unavailableReason = describeWorktreeScanFailure(err) + const failureKind = classifyWorktreeScanFailure(unavailableReason) if (repo.connectionId) { const worktrees = listDisconnectedSshWorktrees(store, repo, sshWorktreeMetaIndex()) return { @@ -170,7 +172,8 @@ export async function listDetectedWorktreesForCapturedRepo( authoritative: false, source: 'metadata-fallback', worktrees: buildDisconnectedDetectedWorktrees(store, repo, worktrees), - unavailableReason + unavailableReason, + failureKind } } return { @@ -178,7 +181,8 @@ export async function listDetectedWorktreesForCapturedRepo( authoritative: false, source: 'metadata-fallback', worktrees: [], - unavailableReason + unavailableReason, + failureKind } } } diff --git a/src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.tsx b/src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.tsx index a956598140e..bcc08529218 100644 --- a/src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.tsx +++ b/src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.tsx @@ -1,16 +1,29 @@ import React from 'react' import { TriangleAlert } from 'lucide-react' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import type { Repo } from '../../../../../../shared/repo-types' import { getRepoExecutionHostId } from '../../../../../../shared/execution-host' +import { + classifyWorktreeScanFailure, + type WorktreeScanFailureKind +} from '../../../../../../shared/worktree-scan-failure' import { handleRepoHeaderActionPointerDown, stopRepoHeaderKeyboardToggle } from './header-event-guards' +const WORKTREE_SCAN_FIX_COMMANDS = { + 'xcode-license': 'sudo xcodebuild -license', + 'developer-tools': 'xcode-select --install' +} as const satisfies Partial> + +function fixCommandForFailureKind(kind: WorktreeScanFailureKind): string | undefined { + return WORKTREE_SCAN_FIX_COMMANDS[kind] +} + /** * Marks a repo whose worktree scan failed, so its rows are retained but cannot be trusted. * Click re-runs the scan: the failure is otherwise re-tried only by the next incidental refresh. @@ -31,45 +44,110 @@ export function RepoScanUnavailableIndicator({ repo }: { repo: Repo }): React.JS 'auto.components.sidebar.RepoScanUnavailableIndicator.retry', 'Retry scan' ) + const executionHostId = getRepoExecutionHostId(repo) + const isLocalHost = executionHostId === 'local' && !repo.connectionId + const isLocalMac = isLocalHost && navigator.userAgent.includes('Mac') + const failureKind: WorktreeScanFailureKind = + detected.failureKind ?? + (isLocalMac ? classifyWorktreeScanFailure(detected.unavailableReason) : 'unknown') + const failureMessageByKind: Partial> = { + 'xcode-license': translate( + 'auto.components.sidebar.RepoScanUnavailableIndicator.xcodeLicense', + 'Apple developer tools require license acceptance before Git can run.' + ), + 'developer-tools': translate( + 'auto.components.sidebar.RepoScanUnavailableIndicator.developerTools', + 'Apple command-line developer tools are missing or unavailable.' + ), + 'architecture-mismatch': translate( + 'auto.components.sidebar.RepoScanUnavailableIndicator.architectureMismatch', + 'A Git-related executable could not run because its CPU architecture is incompatible with this execution host. Install Git and related tools for the host architecture.' + ) + } + const failureMessage = failureMessageByKind[failureKind] ?? detected.unavailableReason + const fixCommand = isLocalMac ? fixCommandForFailureKind(failureKind) : undefined + const diagnosticText = [ + `Repository: ${repo.displayName}`, + ...(isLocalMac + ? [`Path: ${repo.path}`, 'Client platform: macOS'] + : [`Execution host: ${executionHostId}`]), + `Failure: ${detected.unavailableReason}` + ].join('\n') + const copyText = async (value: string): Promise => { + await window.api.ui.writeClipboardText(value) + } return ( - - - - - -
-
{title}
-
{detected.unavailableReason}
-
- {translate( - 'auto.components.sidebar.RepoScanUnavailableIndicator.retained', - 'Existing worktrees are kept until a scan succeeds. Click to retry.' + + + + + + +
+
{title}
+
{failureMessage}
+ {fixCommand ? ( +
+
+ {fixCommand} +
+
+ ) : null} +
+ {translate( + 'auto.components.sidebar.RepoScanUnavailableIndicator.retained', + 'Existing worktrees are kept until a scan succeeds. Click to retry.' + )} +
+
+ {fixCommand ? ( + + ) : null} + +
-
- - + + + ) } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 709597aec8f..00c2bfe2514 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -6356,7 +6356,12 @@ "RepoScanUnavailableIndicator": { "title": "Worktree scan failed for {{value0}}", "retry": "Retry scan", - "retained": "Existing worktrees are kept until a scan succeeds. Click to retry." + "retained": "Existing worktrees are kept until a scan succeeds. Click to retry.", + "xcodeLicense": "Apple developer tools require license acceptance before Git can run.", + "developerTools": "Apple command-line developer tools are missing or unavailable.", + "architectureMismatch": "A Git-related executable could not run because its CPU architecture is incompatible with this execution host. Install Git and related tools for the host architecture.", + "copyCommand": "Copy command", + "copyDiagnostics": "Copy diagnostics" } }, "shared": { diff --git a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-host-merge.ts b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-host-merge.ts index f8078a9925c..b1fb7f7bafb 100644 --- a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-host-merge.ts +++ b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-host-merge.ts @@ -23,6 +23,7 @@ export function mergeDetectedWorktreesForHost( current.authoritative === refreshed.authoritative && current.source === refreshed.source && current.unavailableReason === refreshed.unavailableReason && + current.failureKind === refreshed.failureKind && current.worktrees === worktrees ) { return current diff --git a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-provider-request.ts b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-provider-request.ts index e85add4fc85..4f13b4a6105 100644 --- a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-provider-request.ts +++ b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-provider-request.ts @@ -16,6 +16,7 @@ import type { } from './worktree-slice-types' import { isRuntimeMethodNotFoundError } from './runtime-worktree-rpc-errors' import { toLegacyDetectedWorktreeResult } from './worktree-host-ownership' +import { isWorktreeScanFailureKind } from '../../../../../../shared/worktree-scan-failure' export async function listDetectedWorktreesForRepo( settings: AppState['settings'], @@ -86,7 +87,8 @@ export function isDetectedWorktreeListResult(value: unknown): value is DetectedW (result.source === 'git' || result.source === 'metadata-fallback' || result.source === 'session-fallback') && - Array.isArray(result.worktrees) + Array.isArray(result.worktrees) && + (result.failureKind === undefined || isWorktreeScanFailureKind(result.failureKind)) ) } diff --git a/src/renderer/src/store/slices/worktrees/listing/worktree-catalog-visibility.ts b/src/renderer/src/store/slices/worktrees/listing/worktree-catalog-visibility.ts index c6d3a9636f7..2fcf8d51d28 100644 --- a/src/renderer/src/store/slices/worktrees/listing/worktree-catalog-visibility.ts +++ b/src/renderer/src/store/slices/worktrees/listing/worktree-catalog-visibility.ts @@ -15,6 +15,7 @@ export function areDetectedWorktreeResultsEqual( current.authoritative === next.authoritative && current.source === next.source && current.unavailableReason === next.unavailableReason && + current.failureKind === next.failureKind && catalogRowsEqual(current.worktrees, next.worktrees) ) } diff --git a/src/shared/worktree-scan-failure.test.ts b/src/shared/worktree-scan-failure.test.ts new file mode 100644 index 00000000000..0981344ec4d --- /dev/null +++ b/src/shared/worktree-scan-failure.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest' +import { classifyWorktreeScanFailure } from './worktree-scan-failure' + +describe('classifyWorktreeScanFailure', () => { + it('recognizes Xcode license failures', () => { + expect( + classifyWorktreeScanFailure('Agreeing to the Xcode/iOS license requires admin privileges') + ).toBe('xcode-license') + }) + it('recognizes missing developer tools', () => { + expect(classifyWorktreeScanFailure('xcode-select: error: no developer tools were found')).toBe( + 'developer-tools' + ) + }) + it('does not prescribe installation for an unspecified xcode-select path error', () => { + expect(classifyWorktreeScanFailure('xcode-select: error: invalid active developer path')).toBe( + 'unknown' + ) + }) + it('recognizes architecture spawn failures', () => { + expect(classifyWorktreeScanFailure('spawn Unknown system error -86')).toBe( + 'architecture-mismatch' + ) + }) + it('keeps unrecognized failures unknown', () => { + expect(classifyWorktreeScanFailure('git failed for an unspecified reason')).toBe('unknown') + }) +}) diff --git a/src/shared/worktree-scan-failure.ts b/src/shared/worktree-scan-failure.ts new file mode 100644 index 00000000000..80c9d2db810 --- /dev/null +++ b/src/shared/worktree-scan-failure.ts @@ -0,0 +1,25 @@ +export const WORKTREE_SCAN_FAILURE_KINDS = [ + 'xcode-license', + 'developer-tools', + 'architecture-mismatch', + 'unknown' +] as const + +export type WorktreeScanFailureKind = (typeof WORKTREE_SCAN_FAILURE_KINDS)[number] + +export function isWorktreeScanFailureKind(value: unknown): value is WorktreeScanFailureKind { + return WORKTREE_SCAN_FAILURE_KINDS.some((kind) => kind === value) +} + +export function classifyWorktreeScanFailure(reason: string): WorktreeScanFailureKind { + if (/Agreeing to the Xcode\/iOS license requires admin privileges/i.test(reason)) { + return 'xcode-license' + } + if (/no developer tools were found/i.test(reason)) { + return 'developer-tools' + } + if (/Unknown system error -86|EBADARCH|Bad CPU type in executable/i.test(reason)) { + return 'architecture-mismatch' + } + return 'unknown' +} diff --git a/src/shared/worktree/types.ts b/src/shared/worktree/types.ts index e368716dc01..e07696016a9 100644 --- a/src/shared/worktree/types.ts +++ b/src/shared/worktree/types.ts @@ -6,6 +6,7 @@ import type { DiffComment, MobileDiffReviewState } from '../diff-comment-types' import type { EphemeralVmCheckoutMode } from '../orca-yaml-hook-types' import type { BuiltInWorktreeVisibilitySourceId } from '../repo-types' import type { WorktreeIdentity } from './identity' +import type { WorktreeScanFailureKind } from '../worktree-scan-failure' export type WorkspaceLinkedItem = { provider: 'github' | 'gitlab' | 'linear' | 'jira' @@ -223,4 +224,6 @@ export type DetectedWorktreeListResult = { worktrees: DetectedWorktree[] /** Why a non-authoritative listing could not be scanned; additive, older hosts omit it. */ unavailableReason?: string + /** Structured cause captured by the execution host when a scan fails. */ + failureKind?: WorktreeScanFailureKind } From 9c921360090667e02363fe6e4dee98410665427a Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:55:41 -0400 Subject: [PATCH 03/59] fix(relay): probe a half-open control socket instead of waiting out the silence bound (STA-7672) (#21076) * fix(relay): probe a half-open control socket instead of waiting out the silence bound (STA-7672) A socket CLOSE rejects a pending request as relay_control_closed_, so a relay_control_request_timeout is positive proof the socket stayed open and simply never replied. The only thing that reaps such a socket is RELAY_CONTROL_SILENCE_LIMIT_MS = 75_000, combed every 15s, against a 10s request deadline. On Windows behind NAT/VPN or across sleep-resume a half-open TCP socket accepts send() into a dead pipe and stays invisible for 75-90s, so every pairing attempt in that window times out. The reporter burned ~7. A request that times out with no inbound frame since its send now arms an RFC 6455 ping probe. Terminating on the timeout alone was rejected: relay control ops run DB transactions that can outlive the deadline, and the existing comment in handleMessage records that self-closing on a late reply was strictly worse than ignoring it -- it orphaned the relay session and answered the phone with HOST_OFFLINE for minutes. The probe distinguishes the two cases instead of guessing. The probe deadline deliberately exceeds the relay's own 15s application-level ping cadence. Relay liveness never depended on RFC 6455 control frames surviving end to end, so a shorter window would let a middlebox that swallows pongs turn every request timeout into a reconnect loop. At 20s a healthy cell clears the probe either way -- with a pong, or with the ping it was going to send anyway -- so a probe that fires means the pipe carried neither. Detection drops from 75-90s to ~30s. A pong clears a probe but deliberately does not feed the silence watchdog: it proves the pipe, not that the relay still indexes the session. The timeout error also stops being a bare string; it now names the request kind, the cell, the socket age, the time since the last inbound frame, and whether a probe was armed. The silence watchdog, the probe and the socket age now live in one RelayControlLiveness owner rather than scattered across RelayControlClient. * fix(relay): require a run of unanswered probes before tearing down a control A single unanswered probe was treated as proof of a dead pipe. STA-3320 already established that it is not: a cellular/VPN blackhole or a stalled TCP retransmit routinely swallows one pong from a peer that is still there, which is why RemoteRuntimeServerHeartbeat requires three consecutive misses. The networks this detection exists for are exactly the ones that drop a lone frame, so the first cut was more trigger-happy than the rest of the product. Three changes, all aimed at the cost of a false positive rather than the detection itself: - Three consecutive unanswered probes are now required. The interval drops to 8s so the full run (24s) still outlasts the relay's 15s application-level ping, preserving the property that a healthy cell clears the probe even where a middlebox swallows RFC 6455 control frames. Detection lands at ~34s rather than ~30s, against 75-90s before the fix. Any inbound frame retires the whole run, so a later probe never inherits an earlier miss. - The deadline carries the fleet's existing +/-10% jitter (RELAY_RENEWAL_JITTER_RATIO). Without it every host timing out against one slow cell would probe and terminate on the same boundary -- the synchronized cohort burst that constant was introduced for. The pre-existing 75s watchdog comb has the same defect; this path does not add to it. - A liveness teardown now names its cause in the log. It reaches the origin as an ordinary 1006 close, so without a label a probe-driven reconnect is indistinguishable from any other drop, and a fleet-wide false positive would be invisible in exactly the incident where it matters. Mutation-checked: a miss limit of 1 fails four tests, 2 fails one, and removing the jitter fails one. * fix(relay): keep the request-timeout rejection classifiable The diagnostics added in the previous commit were appended to the rejection's message, which silently destroyed the signal they were meant to add. `mobileRelayMintFailureFromUnknown` classifies a relay failure by testing `error.message` against an anchored `/^relay_[a-z0-9_]{1,74}$/`, so `relay_control_request_timeout reqKind=invite cell=...` stopped matching and every pairing timeout was reported as the generic `relay_mint_failed` instead -- in exactly the flow STA-7672 is about. The pairing path logs only the resolved code and discards the rejection's text, so nothing ever surfaced the suffix: the change was a net loss of diagnosis. The message is bare again and the diagnostics are logged from RelayControlLiveness, which is the only place they survive. Added relay-control-timeout-classification.test.ts to pin the contract end to end through the real classifier, since the coupling is invisible at both sites: restoring the suffix turns the assertion into relay_mint_failed. Found in adversarial review. * refactor(relay): collapse the half-open detection onto one object Design review of the three commits on this branch. No behaviour change: the 184 relay tests pass unmodified, and reverting PROBE_MISS_LIMIT to 1 or 2, or dropping the jitter, still fails them. Dead plumbing. `probeIntervalMs` had zero callers across three layers (client options -> conditional spread -> liveness default), and `silenceLimitMs` the same -- the only production construction site, relay-control-origin.ts, passes neither. Both are gone. `livenessRandom` stays; one test uses it. The conditional-spread idiom went with them: `exactOptionalPropertyTypes` is off for src/ (only cloud/apps/relay-ops sets it), so it bought nothing that `?? Math.random` does not already do. Teardown owns its own log. A two-member reason union crossed a module boundary just to reach a console.warn, and the client re-derived `cell=` from relayOrigin when liveness already held `cellUrl`. Liveness now tears itself down and calls `terminate`; the client lost the import, the method, and the exported type. One probe object, one interval. `probeTimer` + `missedProbes` are now `probe: { timer, misses } | null`, so "no timer implies no misses" is structural instead of maintained by resetting in two places, and the sendProbe/onProbeUnanswered mutual recursion is a plain setInterval. Jitter is computed once per run rather than per tick -- one offset already desynchronizes the cohort. Honest probe label. If ping() throws, the old arm path returned false and the caller logged `probe=in-flight/0` moments after terminating the socket -- a false statement in the line that exists for incident forensics. The arm path now returns the label it means, including `probe=send-failed`. Absorbed RelayControlSilenceWatchdog. It had one consumer and no test file, and this branch had to punch a `lastInboundTime` getter through it purely so liveness could read state it holds. `lastInboundAt` now sits next to `openedAt`; the file, the getter, the import, and the onDead('silence-limit') lambda are all gone. Also: dropped `RelayControlRequestTimeout.reqId` and `PendingRequest.sentAt` (both written, never read -- the timeout closure captures the local `sentAt`); dropped the two `'n/a'` branches, unreachable because a request timeout can only fire after sendActive succeeded, which requires a state only handleProofMessage reaches on the line before it calls liveness.start(); moved the classifier invariant off a void-returning callback type and onto REQUEST_TIMEOUT_CODE, where an edit to the string is next to the warning about editing the string; and replaced the `live` parameter with an `isLive()` option so liveness asks rather than being told, which also let `liveness` be constructed before `requests` instead of a closure reading a field assigned on a later line. --- .../relay/relay-control-client-options.ts | 3 +- .../relay/relay-control-client.test.ts | 162 ++++++++++++++++- .../runtime/relay/relay-control-client.ts | 29 ++-- .../runtime/relay/relay-control-liveness.ts | 163 ++++++++++++++++++ .../runtime/relay/relay-control-requests.ts | 24 ++- .../relay/relay-control-silence-watchdog.ts | 37 ---- ...lay-control-timeout-classification.test.ts | 34 ++++ src/shared/mobile-relay-mint-failure.test.ts | 2 + 8 files changed, 400 insertions(+), 54 deletions(-) create mode 100644 src/main/runtime/relay/relay-control-liveness.ts delete mode 100644 src/main/runtime/relay/relay-control-silence-watchdog.ts create mode 100644 src/main/runtime/relay/relay-control-timeout-classification.test.ts diff --git a/src/main/runtime/relay/relay-control-client-options.ts b/src/main/runtime/relay/relay-control-client-options.ts index 93d1efc0fc6..a5815852242 100644 --- a/src/main/runtime/relay/relay-control-client-options.ts +++ b/src/main/runtime/relay/relay-control-client-options.ts @@ -18,5 +18,6 @@ export type RelayControlClientOptions = { onPendingChanged?: () => void createSocket?: (url: string, relayJwt: string) => WebSocket connectDeadlineMs?: number - silenceLimitMs?: number + // Test seam: deterministic probe jitter. + livenessRandom?: () => number } diff --git a/src/main/runtime/relay/relay-control-client.test.ts b/src/main/runtime/relay/relay-control-client.test.ts index 6896b1064e7..1360e8c4e66 100644 --- a/src/main/runtime/relay/relay-control-client.test.ts +++ b/src/main/runtime/relay/relay-control-client.test.ts @@ -460,12 +460,32 @@ class FakeControlSocket extends EventEmitter { this.close(1006) } + pings = 0 + + ping(): void { + if (this.readyState !== 1) { + throw new Error('socket_not_open') + } + this.pings += 1 + } + + /** The RFC 6455 reply a live peer owes any ping, delivered out of band. */ + pong(): void { + this.emit('pong') + } + deliver(message: ControlFrame): void { this.emit('message', JSON.stringify(message), false) } } -function scriptedControl(options: { closeWithAck?: boolean; issuedAtOffsetMs?: number } = {}): { +function scriptedControl( + options: { + closeWithAck?: boolean + issuedAtOffsetMs?: number + livenessRandom?: () => number + } = {} +): { client: RelayControlClient socket: FakeControlSocket onConnectionOpen: ReturnType @@ -539,6 +559,8 @@ function scriptedControl(options: { closeWithAck?: boolean; issuedAtOffsetMs?: n const onConnectionOpen = vi.fn() const client = new RelayControlClient({ cellUrl: origin, + // Midpoint random => no jitter, so probe boundaries are exact in tests. + livenessRandom: options.livenessRandom ?? (() => 0.5), relayJwt: 'scoped-token', relayHostId, assignmentEpoch: 3, @@ -678,3 +700,141 @@ describe('RelayControlClient scripted-socket lifecycle', () => { expect(onClose).toHaveBeenCalledWith(MOBILE_RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL) }) }) + +// STA-7672: a Windows desktop behind NAT/VPN (or resuming from sleep) can hold a +// half-open control socket that send() writes into happily while nothing comes +// back. Every pairing request then failed at its 10s deadline against a socket +// the 75s silence watchdog would not reap for another minute-plus. +describe('RelayControlClient half-open recovery', () => { + afterEach(() => { + vi.useRealTimers() + }) + + it('tears down only after a run of unanswered probes, not the first one', async () => { + vi.useFakeTimers() + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const { client, socket, onClose } = scriptedControl() + await client.connect() + const invite = client.createInvite('device-1').catch((error: Error) => error.message) + + await vi.advanceTimersByTimeAsync(10_000) + + // The request deadline alone must not close the control — a close would have + // rejected as relay_control_closed_ instead. + expect(await invite).toBe('relay_control_request_timeout') + expect(socket.pings).toBe(1) + expect(socket.readyState).toBe(1) + + // One unanswered probe is UNKNOWN, not death (STA-3320): a lone swallowed + // pong is routine on exactly the VPN/cellular paths this detection targets. + await vi.advanceTimersByTimeAsync(8_000) + expect(socket.pings).toBe(2) + expect(socket.readyState).toBe(1) + await vi.advanceTimersByTimeAsync(8_000) + expect(socket.pings).toBe(3) + expect(socket.readyState).toBe(1) + + // Third consecutive miss is evidence. + await vi.advanceTimersByTimeAsync(8_000) + expect(socket.readyState).toBe(3) + expect(onClose).toHaveBeenCalledWith(1006) + expect(client.isLive()).toBe(false) + // Named in the log so a fleet-wide false positive would be visible. + expect(warn).toHaveBeenCalledWith(expect.stringContaining('reason=probe-unanswered')) + warn.mockRestore() + }) + + it('retires the whole probe run on a single pong', async () => { + vi.useFakeTimers() + const { client, socket, onClose } = scriptedControl() + await client.connect() + const invite = client.createInvite('device-1').catch((error: Error) => error.message) + + await vi.advanceTimersByTimeAsync(10_000) + await invite + await vi.advanceTimersByTimeAsync(8_000) + expect(socket.pings).toBe(2) + socket.pong() + + // A later probe run must start from zero, not inherit the earlier miss. + await vi.advanceTimersByTimeAsync(40_000) + expect(socket.readyState).toBe(1) + expect(onClose).not.toHaveBeenCalled() + expect(client.isLive()).toBe(true) + }) + + it("clears an armed probe on the relay's next ping, with no pong involved", async () => { + vi.useFakeTimers() + const { client, socket, onClose } = scriptedControl() + await client.connect() + const invite = client.createInvite('device-1').catch((error: Error) => error.message) + + await vi.advanceTimersByTimeAsync(10_000) + expect(socket.pings).toBe(1) + await invite + + // The probe run (3 x 8s) outlasts the relay's 15s ping cadence on purpose: + // relay liveness runs at the application layer, so a middlebox that swallows + // RFC 6455 control frames must not be able to make this a reconnect loop. + await vi.advanceTimersByTimeAsync(15_000) + socket.deliver({ type: 'ping', t: Date.now() }) + await vi.advanceTimersByTimeAsync(40_000) + + expect(socket.readyState).toBe(1) + expect(onClose).not.toHaveBeenCalled() + expect(client.isLive()).toBe(true) + }) + + it('does not probe a control that kept talking while a request went unanswered', async () => { + vi.useFakeTimers() + const { client, socket } = scriptedControl() + await client.connect() + const invite = client.createInvite('device-1').catch((error: Error) => error.message) + + await vi.advanceTimersByTimeAsync(5_000) + socket.deliver({ type: 'ping', t: Date.now() }) + await vi.advanceTimersByTimeAsync(5_000) + + // A reply running past its deadline under relay DB load is not a dead + // socket; tearing this control down would strand every phone on the cell. + expect(await invite).toBe('relay_control_request_timeout') + expect(socket.pings).toBe(0) + expect(client.isLive()).toBe(true) + }) + + it('spreads probe deadlines so one slow cell cannot synchronize a cohort', async () => { + vi.useFakeTimers() + // Earliest jitter (-10%) fires at 7.2s; the unjittered boundary is 8s. + const { client, socket } = scriptedControl({ livenessRandom: () => 0 }) + await client.connect() + void client.createInvite('device-1').catch(() => undefined) + + await vi.advanceTimersByTimeAsync(10_000) + expect(socket.pings).toBe(1) + await vi.advanceTimersByTimeAsync(7_300) + expect(socket.pings).toBe(2) + }) + + it('logs the cell and the silence without altering the rejection', async () => { + vi.useFakeTimers() + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const { client } = scriptedControl() + await client.connect() + const invite = client.createInvite('device-1').catch((error: Error) => error.message) + + await vi.advanceTimersByTimeAsync(10_000) + + // The message is a classification key: mobile-relay-mint-failure.ts matches + // it against an anchored /^relay_[a-z0-9_]{1,74}$/, so a diagnostic suffix + // silently downgrades this to the generic relay_mint_failed fallback. + expect(await invite).toBe('relay_control_request_timeout') + + const logged = warn.mock.calls.map((call) => String(call[0])).join('\n') + expect(logged).toContain('reqKind=invite') + expect(logged).toContain('cell=http://relay.test') + expect(logged).toContain('socketAgeMs=10000') + expect(logged).toContain('sinceInboundMs=10000') + expect(logged).toContain('probe=armed') + warn.mockRestore() + }) +}) diff --git a/src/main/runtime/relay/relay-control-client.ts b/src/main/runtime/relay/relay-control-client.ts index e60c4a58de8..ea2632d11e5 100644 --- a/src/main/runtime/relay/relay-control-client.ts +++ b/src/main/runtime/relay/relay-control-client.ts @@ -18,10 +18,7 @@ import { import { RelayControlRequests } from './relay-control-requests' import type { DeviceCredentialInstallAuthorization } from './relay-control-requests' import { answerRelayHostChallenge } from './relay-host-proof' -import { - RELAY_CONTROL_SILENCE_LIMIT_MS, - RelayControlSilenceWatchdog -} from './relay-control-silence-watchdog' +import { RelayControlLiveness } from './relay-control-liveness' import { closeRelayControlSocket } from './relay-control-socket-close' import { controlWebSocketUrl } from './relay-control-url' @@ -34,23 +31,28 @@ export class RelayControlClient { private readonly relayOrigin: string private readonly controlUrl: string private readonly createSocket: NonNullable + private readonly liveness: RelayControlLiveness private readonly requests: RelayControlRequests private socket: WebSocket | null = null private state: RelayControlState = 'idle' private connectResolve: ((ack: RelayHostHelloAckMessage) => void) | null = null private connectReject: ((error: Error) => void) | null = null private connectTimer: ReturnType | null = null - private readonly silenceWatchdog: RelayControlSilenceWatchdog constructor(options: RelayControlClientOptions) { this.options = options - this.requests = new RelayControlRequests(options.onPendingChanged) const endpoint = controlWebSocketUrl(options.cellUrl) this.relayOrigin = endpoint.origin this.controlUrl = endpoint.url - this.silenceWatchdog = new RelayControlSilenceWatchdog( - options.silenceLimitMs ?? RELAY_CONTROL_SILENCE_LIMIT_MS, - () => this.socket?.terminate() + this.liveness = new RelayControlLiveness({ + cellUrl: this.relayOrigin, + ping: () => this.socket?.ping(), + isLive: () => this.isLive(), + terminate: () => this.socket?.terminate(), + random: options.livenessRandom + }) + this.requests = new RelayControlRequests(options.onPendingChanged, (timeout) => + this.liveness.noteRequestTimeout(timeout) ) this.createSocket = options.createSocket ?? @@ -70,8 +72,9 @@ export class RelayControlClient { const socket = this.createSocket(this.controlUrl, this.options.relayJwt) this.socket = socket socket.once('open', () => this.sendHostHello()) + socket.on('pong', () => this.liveness.notePong()) socket.on('message', (raw, isBinary) => { - this.silenceWatchdog.noteInbound() + this.liveness.noteInbound() if (isBinary) { this.failProtocol('binary control message') return @@ -157,7 +160,7 @@ export class RelayControlClient { closeNow(hostCloseReason?: RelayHostCloseReason): void { const wasConnecting = this.state === 'opening' || this.state === 'proving' this.state = 'closed' - this.silenceWatchdog.stop() + this.liveness.stop() if (wasConnecting) { this.connectReject?.(new Error('relay_control_closed')) this.clearConnectPromise() @@ -267,7 +270,7 @@ export class RelayControlClient { return } this.state = 'active' - this.silenceWatchdog.start() + this.liveness.start() this.connectResolve?.(ack.data) this.clearConnectPromise() } @@ -288,7 +291,7 @@ export class RelayControlClient { private handleClose(code: number): void { const wasConnecting = this.state === 'opening' || this.state === 'proving' this.state = 'closed' - this.silenceWatchdog.stop() + this.liveness.stop() if (wasConnecting) { this.connectReject?.(new Error(`relay_control_closed_${code}`)) this.clearConnectPromise() diff --git a/src/main/runtime/relay/relay-control-liveness.ts b/src/main/runtime/relay/relay-control-liveness.ts new file mode 100644 index 00000000000..0a6bef02d60 --- /dev/null +++ b/src/main/runtime/relay/relay-control-liveness.ts @@ -0,0 +1,163 @@ +import type { RelayControlRequestTimeout } from './relay-control-requests' +import { RELAY_RENEWAL_JITTER_RATIO } from './relay-renewal-jitter' + +// STA-7672: a control request that times out has two indistinguishable causes — +// a loaded relay whose reply is late, or a half-open TCP socket that swallowed +// the send (common on Windows behind NAT/VPN or across sleep-resume, where the +// OS reports the write as succeeding). A close would have rejected as +// `relay_control_closed_`, so a timeout proves the socket stayed open and +// never answered. An RFC 6455 ping settles which cause it was without spending +// an application opcode: any live peer must answer with a pong. + +// The relay pings every 15s and closes a control after 75s of silence; mirror +// that bound so a dead or server-side-unindexed socket cannot stay "active". +const SILENCE_LIMIT_MS = 75_000 +const SILENCE_CHECK_INTERVAL_MS = 15_000 + +// Three of these resolve a suspect socket in 24s, well inside the silence bound +// above — which on Windows let a user burn every pairing attempt before it fired. +const PROBE_INTERVAL_MS = 8_000 + +// One unanswered probe is UNKNOWN, not death: a cellular/VPN blackhole or a +// stalled TCP retransmit routinely swallows a lone pong (STA-3320). The run this +// requires also makes the window (24s) outlast the relay's own 15s ping, so a +// middlebox that swallows every pong still cannot force a reconnect loop — the +// cell's ping lands inside the window and clears the run. A teardown therefore +// means the pipe carried neither frame, three times over. +const PROBE_MISS_LIMIT = 3 + +type TeardownReason = 'probe-unanswered' | 'silence-limit' + +export type RelayControlLivenessOptions = { + cellUrl: string + ping: () => void + isLive: () => boolean + terminate: () => void + random?: () => number +} + +/** Everything that decides whether a control socket is still reachable. */ +export class RelayControlLiveness { + private readonly random: () => number + private probe: { timer: ReturnType; misses: number } | null = null + private silenceTimer: ReturnType | null = null + private openedAt = 0 + private lastInboundAt = 0 + + constructor(private readonly options: RelayControlLivenessOptions) { + this.random = options.random ?? Math.random + } + + start(): void { + this.openedAt = Date.now() + this.lastInboundAt = this.openedAt + this.silenceTimer = setInterval(() => { + if (Date.now() - this.lastInboundAt > SILENCE_LIMIT_MS) { + this.tearDown('silence-limit') + } + }, SILENCE_CHECK_INTERVAL_MS) + this.silenceTimer.unref?.() + } + + noteInbound(): void { + this.lastInboundAt = Date.now() + this.clearProbe() + } + + // A pong proves the pipe and nothing more — it can come from a socket the + // relay has already unindexed — so it clears a probe but never advances + // `lastInboundAt`, whose job is to mirror the relay's own 75s bound. + notePong(): void { + this.clearProbe() + } + + stop(): void { + if (this.silenceTimer) { + clearInterval(this.silenceTimer) + this.silenceTimer = null + } + this.clearProbe() + } + + /** + * Probe only when nothing at all arrived since the send: then the relay's own + * ping is overdue too, which is the half-open signature rather than a reply + * running late under load. Otherwise this just records why the request failed. + */ + noteRequestTimeout(timeout: RelayControlRequestTimeout): void { + const now = Date.now() + const diagnostics = [ + `reqKind=${timeout.kind}`, + `cell=${this.options.cellUrl}`, + `socketAgeMs=${now - this.openedAt}`, + `sinceInboundMs=${now - this.lastInboundAt}` + ] + if (this.options.isLive() && this.lastInboundAt <= timeout.sentAt) { + diagnostics.push(this.armProbe()) + } + // Logged rather than appended to the rejection, which is a classification + // key; the pairing flow discards the rejection's text entirely. + console.warn(`[relay] control request timed out ${diagnostics.join(' ')}`) + } + + /** Starts a probe run if none is live; returns what to report in the log. */ + private armProbe(): string { + if (this.probe) { + return `probe=in-flight/${this.probe.misses}` + } + if (!this.sendProbe()) { + return 'probe=send-failed' + } + // One jitter offset per run is enough to keep a cohort timing out against + // the same slow cell off a shared boundary (see RELAY_RENEWAL_JITTER_RATIO). + const spread = (this.random() * 2 - 1) * RELAY_RENEWAL_JITTER_RATIO + const timer = setInterval( + () => this.onProbeMissed(), + Math.max(1, Math.floor(PROBE_INTERVAL_MS * (1 + spread))) + ) + timer.unref?.() + this.probe = { timer, misses: 0 } + return 'probe=armed' + } + + private onProbeMissed(): void { + const probe = this.probe + if (!probe) { + return + } + probe.misses += 1 + if (probe.misses >= PROBE_MISS_LIMIT) { + this.tearDown('probe-unanswered') + return + } + this.sendProbe() + } + + private sendProbe(): boolean { + try { + this.options.ping() + return true + } catch { + // A ping that throws on a live control is already the answer. + this.tearDown('probe-unanswered') + return false + } + } + + /** Any inbound frame — pong or application message — retires the probe run. */ + private clearProbe(): void { + if (this.probe) { + clearInterval(this.probe.timer) + this.probe = null + } + } + + // A teardown lands on the origin as an ordinary 1006 close, so name the cause + // here: without it a probe-driven reconnect is indistinguishable from any + // other drop, and a fleet-wide false positive would be invisible. + private tearDown(reason: TeardownReason): void { + this.stop() + console.warn(`[relay] control torn down cell=${this.options.cellUrl} reason=${reason}`) + this.options.terminate() + } +} diff --git a/src/main/runtime/relay/relay-control-requests.ts b/src/main/runtime/relay/relay-control-requests.ts index 6151d634f0d..eeba886be23 100644 --- a/src/main/runtime/relay/relay-control-requests.ts +++ b/src/main/runtime/relay/relay-control-requests.ts @@ -18,6 +18,20 @@ type PendingRequest = { timer: ReturnType } +export type RelayControlRequestTimeout = { + kind: PendingRequest['kind'] + sentAt: number +} + +/** Notified when a request hits its deadline, so liveness can probe the socket. */ +export type OnRelayControlRequestTimeout = (timeout: RelayControlRequestTimeout) => void + +// A classification key, not prose: consumers exact-match this against +// /^relay_[a-z0-9_]{1,74}$/ (src/shared/mobile-relay-mint-failure.ts), so any +// appended diagnostic downgrades a precise code to the generic fallback. +// Diagnostics belong in the log — see RelayControlLiveness.noteRequestTimeout. +const REQUEST_TIMEOUT_CODE = 'relay_control_request_timeout' + export type DeviceCredentialInstallAuthorization = | { mode: 'relay-basis'; basisConnId: string } | { mode: 'authenticated-direct'; directAuthId: string } @@ -42,7 +56,10 @@ type SendRelayControlRequest = (payload: RelayControlRequestPayload) => void export class RelayControlRequests { private readonly pending = new Map() - constructor(private readonly onPendingChanged?: () => void) {} + constructor( + private readonly onPendingChanged?: () => void, + private readonly onTimeout?: OnRelayControlRequestTimeout + ) {} get size(): number { return this.pending.size @@ -170,10 +187,13 @@ export class RelayControlRequests { if (this.pending.has(reqId)) { return Promise.reject(new Error('duplicate_relay_request_id')) } + const sentAt = Date.now() return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.finish(reqId) - reject(new Error('relay_control_request_timeout')) + // Runs before the reject so the probe observes the socket as the deadline found it. + this.onTimeout?.({ kind, sentAt }) + reject(new Error(REQUEST_TIMEOUT_CODE)) }, 10_000) this.pending.set(reqId, { kind, resolve, reject, timer }) try { diff --git a/src/main/runtime/relay/relay-control-silence-watchdog.ts b/src/main/runtime/relay/relay-control-silence-watchdog.ts deleted file mode 100644 index 4015dea3dd8..00000000000 --- a/src/main/runtime/relay/relay-control-silence-watchdog.ts +++ /dev/null @@ -1,37 +0,0 @@ -// The relay pings every 15s and closes a control after 75s of silence; mirror -// that bound so a dead or server-side-unindexed socket cannot stay "active". -export const RELAY_CONTROL_SILENCE_LIMIT_MS = 75_000 - -const CHECK_INTERVAL_MS = 15_000 - -export class RelayControlSilenceWatchdog { - private timer: ReturnType | null = null - private lastInboundAt = 0 - - constructor( - private readonly limitMs: number, - private readonly onSilence: () => void - ) {} - - noteInbound(): void { - this.lastInboundAt = Date.now() - } - - start(): void { - this.lastInboundAt = Date.now() - this.timer = setInterval(() => { - if (Date.now() - this.lastInboundAt > this.limitMs) { - this.stop() - this.onSilence() - } - }, CHECK_INTERVAL_MS) - this.timer.unref?.() - } - - stop(): void { - if (this.timer) { - clearInterval(this.timer) - this.timer = null - } - } -} diff --git a/src/main/runtime/relay/relay-control-timeout-classification.test.ts b/src/main/runtime/relay/relay-control-timeout-classification.test.ts new file mode 100644 index 00000000000..ca8f8c1d79e --- /dev/null +++ b/src/main/runtime/relay/relay-control-timeout-classification.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from 'vitest' +import { mobileRelayMintFailureFromUnknown } from '../../../shared/mobile-relay-mint-failure' +import { RelayControlRequests } from './relay-control-requests' + +// A control-request rejection is a classification key, not prose: the pairing +// flow feeds error.message through an anchored /^relay_[a-z0-9_]{1,74}$/ and +// falls back to a generic code on any mismatch. Appending diagnostics to that +// message once turned every pairing timeout into relay_mint_failed, losing the +// one signal that identified STA-7672 — so pin the contract end to end. +describe('control request timeout classification', () => { + it('keeps a timed-out request classifiable by the mobile pairing flow', async () => { + vi.useFakeTimers() + try { + const onTimeout = vi.fn() + const requests = new RelayControlRequests(undefined, onTimeout) + const settled = requests.createInvite('req-1', 'device-1', () => {}).catch((e: Error) => e) + + await vi.advanceTimersByTimeAsync(10_000) + const error = await settled + + expect(onTimeout).toHaveBeenCalledOnce() + expect( + mobileRelayMintFailureFromUnknown({ + error, + stage: 'create_pairing_relay', + fallbackCode: 'relay_mint_failed', + fallbackMessage: 'could not mint' + }).code + ).toBe('relay_control_request_timeout') + } finally { + vi.useRealTimers() + } + }) +}) diff --git a/src/shared/mobile-relay-mint-failure.test.ts b/src/shared/mobile-relay-mint-failure.test.ts index d0d8a768a8b..6a1b47ce8e8 100644 --- a/src/shared/mobile-relay-mint-failure.test.ts +++ b/src/shared/mobile-relay-mint-failure.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest' import { mobileRelayMintFailureFromUnknown } from './mobile-relay-mint-failure' +// The producer end of this contract is pinned separately, in +// src/main/runtime/relay/relay-control-timeout-classification.test.ts. describe('mobileRelayMintFailureFromUnknown', () => { it('keeps known machine-readable Relay codes for diagnostics', () => { expect( From d3032da299b493abcc1d9eaff5020a90f750d65f Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:11:18 -0700 Subject: [PATCH 04/59] fix(renderer): cancel signout auth retry on unmount (#20905) Co-authored-by: m4air --- src/renderer/src/components/UnexpectedSignoutCard.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/UnexpectedSignoutCard.tsx b/src/renderer/src/components/UnexpectedSignoutCard.tsx index dbfb106eb23..40035a8da92 100644 --- a/src/renderer/src/components/UnexpectedSignoutCard.tsx +++ b/src/renderer/src/components/UnexpectedSignoutCard.tsx @@ -59,6 +59,7 @@ export function UnexpectedSignoutCard(): React.JSX.Element | null { useEffect(() => { let cancelled = false let attempts = 0 + let retryTimer: number | null = null const refresh = (): void => { attempts += 1 void useAppStore @@ -71,13 +72,19 @@ export function UnexpectedSignoutCard(): React.JSX.Element | null { if (status != null) { setAuthRefreshReady(true) } else if (attempts < 3) { - window.setTimeout(refresh, 500) + retryTimer = window.setTimeout(() => { + retryTimer = null + refresh() + }, 500) } }) } refresh() return () => { cancelled = true + if (retryTimer !== null) { + window.clearTimeout(retryTimer) + } } }, []) From d7d3bcfc6653592be78ef5c6ea3298153eea4209 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:11:21 -0700 Subject: [PATCH 05/59] fix(renderer): cancel copied prompt reset on unmount (#20906) * fix(renderer): cancel copied prompt reset on unmount * fix: address memory PR review regressions and withdraw false positives --------- Co-authored-by: m4air Co-authored-by: m4air --- .../settings/EphemeralVmsPane.test.tsx | 45 +++++++++++++++++++ .../components/settings/EphemeralVmsPane.tsx | 20 ++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/settings/EphemeralVmsPane.test.tsx b/src/renderer/src/components/settings/EphemeralVmsPane.test.tsx index 8a68688f624..39400a827d8 100644 --- a/src/renderer/src/components/settings/EphemeralVmsPane.test.tsx +++ b/src/renderer/src/components/settings/EphemeralVmsPane.test.tsx @@ -138,6 +138,51 @@ describe('EphemeralVmsPane', () => { }) }) + it('does not schedule a reset when clipboard completion arrives after unmount', async () => { + let finishClipboard!: () => void + vi.mocked(window.api.ui.writeClipboardText).mockReturnValueOnce( + new Promise((resolve) => { + finishClipboard = resolve + }) + ) + const container = await renderPane() + const setTimeout = vi.spyOn(window, 'setTimeout') + try { + await act(async () => { + container.querySelector('button[aria-label="Copy"]')?.click() + }) + await act(async () => roots.pop()?.unmount()) + setTimeout.mockClear() + await act(async () => { + finishClipboard() + await Promise.resolve() + }) + expect(setTimeout.mock.calls.filter(([, delay]) => delay === 1500)).toHaveLength(0) + } finally { + setTimeout.mockRestore() + } + }) + + it('shows copied feedback while mounted and releases its reset on unmount', async () => { + const container = await renderPane() + const setTimeout = vi.spyOn(window, 'setTimeout') + const clearTimeout = vi.spyOn(window, 'clearTimeout') + try { + await act(async () => { + container.querySelector('button[aria-label="Copy"]')?.click() + }) + expect(container.querySelector('button[aria-label="Copy"]')?.textContent).toBe('Copied') + const timerIndex = setTimeout.mock.calls.findIndex(([, delay]) => delay === 1500) + expect(timerIndex).toBeGreaterThanOrEqual(0) + const timer = setTimeout.mock.results[timerIndex].value + await act(async () => roots.pop()?.unmount()) + expect(clearTimeout).toHaveBeenCalledWith(timer) + } finally { + setTimeout.mockRestore() + clearTimeout.mockRestore() + } + }) + it('refreshes the catalog when plugin content changes', async () => { const listRecipeCatalog = window.api.ephemeralVm.listRecipeCatalog as ReturnType const container = await renderPane() diff --git a/src/renderer/src/components/settings/EphemeralVmsPane.tsx b/src/renderer/src/components/settings/EphemeralVmsPane.tsx index acd042444c3..7b0086ae78b 100644 --- a/src/renderer/src/components/settings/EphemeralVmsPane.tsx +++ b/src/renderer/src/components/settings/EphemeralVmsPane.tsx @@ -45,6 +45,15 @@ export function EphemeralVmsPane(): React.JSX.Element { const [promptCopied, setPromptCopied] = useState(false) const mountedRef = useMountedRef() const refreshGenerationRef = useRef(0) + const promptResetTimerRef = useRef(null) + + useEffect(() => { + return () => { + if (promptResetTimerRef.current !== null) { + window.clearTimeout(promptResetTimerRef.current) + } + } + }, []) // Why: an absent runtime still resolves to the local host, which is what the // seven sibling panes rely on to reach the Windows npx preflight. @@ -126,8 +135,17 @@ export function EphemeralVmsPane(): React.JSX.Element { try { await window.api.ui.writeClipboardText(AGENT_PROMPT) useAppStore.getState().recordFeatureInteraction('ephemeral-vm-setup') + if (!mountedRef.current) { + return + } setPromptCopied(true) - setTimeout(() => setPromptCopied(false), 1500) + if (promptResetTimerRef.current !== null) { + window.clearTimeout(promptResetTimerRef.current) + } + promptResetTimerRef.current = window.setTimeout(() => { + promptResetTimerRef.current = null + setPromptCopied(false) + }, 1500) } catch { toast.error( translate( From bd404185f19d077065f926c06574c039a7986392 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:11:49 -0700 Subject: [PATCH 06/59] fix(renderer): release parked terminal scroll intents (#20924) * fix: release scroll intents for closed parked tabs * fix(renderer): release scroll intents on worktree removal * test(renderer): cover parked worktree intent cleanup --------- Co-authored-by: m4air --- ...inal-parked-watcher-reconciliation.test.ts | 39 +++++++++++++++++++ .../terminal-parked-watcher-registry.ts | 17 +++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts index f59d8b4500e..f92d34c00a6 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts @@ -1,12 +1,17 @@ import { afterEach, describe, expect, it } from 'vitest' import { captureParkedTerminalPaneCandidates, + pruneParkedTerminalWatchers, retireParkedTerminalTab } from './terminal-parked-watcher-registry' import { reconcileParkedWatcherPtyIds, resolveParkedTerminalPaneCandidates } from './terminal-parked-watcher-reconciliation' +import { + readTerminalScrollIntentKeyRetention, + writeKeyedTerminalScrollIntent +} from '../../lib/pane-manager/terminal-scroll-intent-key-store' const TAB_ID = 'tab-1' const WORKTREE_ID = 'repo::/worktree' @@ -86,6 +91,40 @@ describe('paired parked-watcher reconciliation', () => { }) }) +it('releases captured scroll-intent keys when a parked tab is closed', () => { + writeKeyedTerminalScrollIntent(FIRST_LEAF_ID, { + kind: 'pinnedViewport', + bufferType: 'normal', + viewportY: 4, + baseY: 12, + revision: 1 + }) + captureParkedTerminalPaneCandidates(TAB_ID, WORKTREE_ID, [ + { ptyId: FIRST_PTY_ID, paneId: 1, leafId: FIRST_LEAF_ID, drivesTabTitle: true } + ]) + + expect(readTerminalScrollIntentKeyRetention().intents).toBe(1) + retireParkedTerminalTab(TAB_ID) + expect(readTerminalScrollIntentKeyRetention().intents).toBe(0) +}) + +it('releases captured scroll-intent keys when a parked worktree is removed', () => { + writeKeyedTerminalScrollIntent(SECOND_LEAF_ID, { + kind: 'pinnedViewport', + bufferType: 'normal', + viewportY: 8, + baseY: 16, + revision: 1 + }) + captureParkedTerminalPaneCandidates(TAB_ID, WORKTREE_ID, [ + { ptyId: FIRST_PTY_ID, paneId: 1, leafId: SECOND_LEAF_ID, drivesTabTitle: true } + ]) + + pruneParkedTerminalWatchers(new Set()) + + expect(readTerminalScrollIntentKeyRetention().intents).toBe(0) +}) + // Why: the sole-newborn parity flag is a fact about the captured PTY, so the // layout-fallback rescue must carry it only while the leaf still binds that PTY. describe('untouchedFreshSpawn carry through the layout-fallback rescue', () => { diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts index 7e305ac7fe3..020421e637c 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts @@ -10,6 +10,7 @@ import { discardPreHandlerPtyState, hasPreHandlerPtyExit } from './pty-pre-handler-buffer' import { parseRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' +import { releaseTerminalScrollIntentKey } from '../../lib/pane-manager/terminal-scroll-intent-key-store' export type ParkedTerminalPaneCapture = { ptyId: string | null @@ -140,7 +141,16 @@ export function retireParkedTerminalTab(tabId: string): void { // Why: explicit tab retirement permanently invalidates both live parked // observers and unmounted-pane candidates; neither may reattach later. disposeParkedTabWatchers(tabId) - capturedPanesByTabId.delete(tabId) + const capture = capturedPanesByTabId.get(tabId) + if (capture) { + // Parked panes never run PaneManager's close teardown. Release their + // strong scroll-intent keys here or every closed parked tab leaks one per + // leaf for the renderer lifetime. + for (const pane of capture.panes) { + releaseTerminalScrollIntentKey(pane.leafId) + } + capturedPanesByTabId.delete(tabId) + } } /** @@ -215,6 +225,11 @@ export function pruneParkedTerminalWatchers(liveWorktreeIds: ReadonlySet } for (const [tabId, capture] of capturedPanesByTabId) { if (!liveWorktreeIds.has(capture.worktreeId)) { + for (const pane of capture.panes) { + // Worktree removal can bypass closeTab while panes are parked; release + // the same strong scroll-intent keys as explicit tab retirement. + releaseTerminalScrollIntentKey(pane.leafId) + } capturedPanesByTabId.delete(tabId) } } From df88f83c707487add7715ed00ee0b754d6c76796 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:11:52 -0700 Subject: [PATCH 07/59] fix(relay): bound descendant traversal on cyclic process snapshots (#20946) Co-authored-by: m4air --- .../pty-shell-utils-process-cycles.test.ts | 55 +++++++++++++++++++ src/relay/pty-shell-utils.ts | 6 ++ 2 files changed, 61 insertions(+) create mode 100644 src/relay/pty-shell-utils-process-cycles.test.ts diff --git a/src/relay/pty-shell-utils-process-cycles.test.ts b/src/relay/pty-shell-utils-process-cycles.test.ts new file mode 100644 index 00000000000..aa074651ced --- /dev/null +++ b/src/relay/pty-shell-utils-process-cycles.test.ts @@ -0,0 +1,55 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getProcessTableIndex } from '../shared/process-table-index' +import type { ProcessTableRow } from '../shared/process-table-snapshot' +import { getProcessTableSnapshot } from '../shared/process-table-snapshot-reader' +import { getForegroundProcessName } from './pty-shell-utils' + +vi.mock(import('../shared/process-table-snapshot-reader'), async (importOriginal) => ({ + ...(await importOriginal()), + getProcessTableSnapshot: vi.fn() +})) + +function row(pid: number, ppid: number, command = 'bash'): ProcessTableRow { + return { pid, ppid, stat: 'S+', command } +} + +describe('relay foreground process snapshot cycles', () => { + let platform: PropertyDescriptor | undefined + + beforeEach(() => { + platform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) + }) + + afterEach(() => { + vi.restoreAllMocks() + if (platform) { + Object.defineProperty(process, 'platform', platform) + } + }) + + it.each([ + ['a self-parented root', [row(100, 100), row(101, 100, 'node /usr/bin/codex')]], + ['a two-process cycle', [row(100, 101), row(101, 100, 'node /usr/bin/codex')]], + [ + 'duplicate rows', + [row(100, 1), row(101, 100, 'node /usr/bin/codex'), row(101, 100, 'node /usr/bin/codex')] + ], + ['an ordinary tree', [row(100, 1), row(101, 100, 'node /usr/bin/codex')]] + ])('resolves the agent once for %s', async (_name, rows) => { + vi.mocked(getProcessTableSnapshot).mockResolvedValue(rows) + const children = getProcessTableIndex(rows).childrenByPpid + const readChildren = children.get.bind(children) + let reads = 0 + vi.spyOn(children, 'get').mockImplementation((pid) => { + // Bound the regression itself so removing the guard cannot OOM the test worker. + if (++reads > 20) { + throw new Error('process snapshot traversal did not terminate') + } + return readChildren(pid) + }) + + await expect(getForegroundProcessName(100, 'node')).resolves.toBe('codex') + expect(reads).toBeLessThanOrEqual(rows.length) + }) +}) diff --git a/src/relay/pty-shell-utils.ts b/src/relay/pty-shell-utils.ts index 9c8585933a1..0deaf8c6dd6 100644 --- a/src/relay/pty-shell-utils.ts +++ b/src/relay/pty-shell-utils.ts @@ -184,9 +184,15 @@ function collectDescendants( rootPid: number ): (ProcessTableRow & { depth: number })[] { const descendants: (ProcessTableRow & { depth: number })[] = [] + const seen = new Set([rootPid]) const stack = (index.childrenByPpid.get(rootPid) ?? []).map((row) => ({ row, depth: 1 })) while (stack.length > 0) { const { row, depth } = stack.pop()! + // Process snapshots can contain duplicate PIDs or cycles during reparenting. + if (seen.has(row.pid)) { + continue + } + seen.add(row.pid) descendants.push({ ...row, depth }) for (const child of index.childrenByPpid.get(row.pid) ?? []) { stack.push({ row: child, depth: depth + 1 }) From e9c04fb8d95254f0cb29addf669d4a597f8ca9a7 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:11:55 -0700 Subject: [PATCH 08/59] fix(ai-vault): ignore cancellations after request settlement (#20980) Co-authored-by: m4air --- docs/audits/scanner-late-cancel/README.md | 51 +++++ docs/audits/scanner-late-cancel/reproduce.mjs | 94 +++++++++ docs/audits/scanner-late-cancel/results.json | 27 +++ ...ssion-scanner-service-cancellation.test.ts | 187 ++++++++++++++++++ .../ai-vault/session-scanner-service-entry.ts | 3 + 5 files changed, 362 insertions(+) create mode 100644 docs/audits/scanner-late-cancel/README.md create mode 100644 docs/audits/scanner-late-cancel/reproduce.mjs create mode 100644 docs/audits/scanner-late-cancel/results.json create mode 100644 src/main/ai-vault/session-scanner-service-cancellation.test.ts diff --git a/docs/audits/scanner-late-cancel/README.md b/docs/audits/scanner-late-cancel/README.md new file mode 100644 index 00000000000..1d4a9a7044b --- /dev/null +++ b/docs/audits/scanner-late-cancel/README.md @@ -0,0 +1,51 @@ +# AI Vault scanner late cancellation + +The scanner child kept cancellation IDs after their requests had already settled. +Its response can still be in transit when the parent sends a cancellation, so this +does not require an invalid caller. The completed request has already run its +cleanup; nothing remains to delete the newly inserted ID. + +The fix admits cancellation only while the existing `pending` set owns the request. +That set includes both queued and running requests. Their cancellation and cleanup +remain unchanged. No protocol or history-retention policy changes. + +## Proof + +Run from the repository root with dependencies installed: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --max-old-space-size=128 docs/audits/scanner-late-cancel/reproduce.mjs +``` + +The script loads the checked-out production entry and derives the before version by +removing only the three-line `pending` membership guard in memory. It checks that +the guard occurs exactly once; no historical commit or Git access is needed. +Esbuild strips TypeScript before both versions run in separate VM contexts. Only +imported collaborators are stubbed: the production message handler, request lanes, +sets, and cleanup run. After each synthetic first-prompt request completes, its +matching cancel arrives. + +The script asserts the counts and emits JSON with both source SHA-256 hashes and +Node/platform/heap-limit provenance. [results.json](./results.json) records a run on +Node v26.6.0 with a 128 MiB old-space limit. This measures retained entries, not RSS. + +| Source | Requests/responses | Pending | Controllers | Retained cancel IDs | +| ------ | -----------------: | ------: | ----------: | ------------------: | +| Before | 1,000 / 1,000 | 0 | 0 | 1,000 | +| After | 1,000 / 1,000 | 0 | 0 | 0 | + +The regression test imports the production entry and directly observes its existing +cancellation set through an admitted cancellation. It repeats late cancels after +both successful and failed requests, verifies queued/running cancellation, and +checks shutdown cleanup. No production diagnostics or test-only exports were added. + +```sh +ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/ai-vault/session-scanner-service-cancellation.test.ts src/main/ai-vault/session-scanner-service-entry.test.ts src/main/ai-vault/session-scanner-service-client.test.ts +``` + +## Incident scope + +The same unconditional insertion exists in release `v1.4.198`. This retains numeric +IDs in the scanner child, not transcript contents in Electron main. It is a concrete +small leak; it does not explain the reported roughly 26 MB/s main-process growth in +#19768 or establish the cause of #19831's scope-level peak memory measurements. diff --git a/docs/audits/scanner-late-cancel/reproduce.mjs b/docs/audits/scanner-late-cancel/reproduce.mjs new file mode 100644 index 00000000000..e0a0b56a2a3 --- /dev/null +++ b/docs/audits/scanner-late-cancel/reproduce.mjs @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { EventEmitter } from 'node:events' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { getHeapStatistics } from 'node:v8' +import { runInNewContext } from 'node:vm' +import { transform } from 'esbuild' + +if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { + throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1') +} + +const sourcePath = 'src/main/ai-vault/session-scanner-service-entry.ts' +const source = readFileSync(new URL(`../../../${sourcePath}`, import.meta.url), 'utf8') +const guard = ' if (!pending.has(raw.id)) {\n return\n }\n' +const requestCount = 1000 +assert.equal(source.split(guard).length, 2, 'Review changed baseline transform') + +async function run(version) { + const original = version === 'before' ? source.replace(guard, '') : source + const { code } = await transform(original, { loader: 'ts', format: 'esm', target: 'es2022' }) + const entry = code.replace(/^import[\s\S]*?from ['"][^'"]+['"];?\n/gm, '') + assert.equal(/^import\b/m.test(entry), false, 'Unexpected production import shape') + const processStub = new EventEmitter() + let responses = 0 + processStub.send = (message) => { + if (message.type === 'result') { + responses++ + } + } + processStub.pid = 1 + const context = { + process: processStub, + performance, + AbortController, + requestSessionSearchRoots: () => undefined, + SessionScannerServiceSearch: class { + handles() { + return false + } + }, + AI_VAULT_SERVICE_PROTOCOL_VERSION: 1, + aiVaultServiceLane: () => 'interactive', + isAiVaultServiceRequest: (raw) => raw.type === 'request', + readAiVaultFirstUserPrompt: async () => ({ prompt: null }), + inspect: undefined + } + runInNewContext( + `${entry}\ninspect = () => ({ pending: pending.size, controllers: controllers.size, cancelled: cancelled.size })`, + context, + { timeout: 1000, filename: fileURLToPath(new URL(`../../../${sourcePath}`, import.meta.url)) } + ) + try { + processStub.emit('message', { type: 'init', protocol: 1 }) + for (let id = 1; id <= requestCount; id++) { + processStub.emit('message', { + type: 'request', + id, + operation: 'firstPrompt', + request: { agent: 'claude', filePath: '/synthetic' } + }) + await new Promise(setImmediate) + processStub.emit('message', { type: 'cancel', id }) + } + const retained = context.inspect() + assert.equal(responses, requestCount) + assert.equal(retained.pending, 0) + assert.equal(retained.controllers, 0) + assert.equal(retained.cancelled, version === 'before' ? requestCount : 0) + return { + source: version === 'before' ? 'working tree without pending guard' : 'working tree', + sourceSha256: createHash('sha256').update(original).digest('hex'), + requests: requestCount, + responses, + ...retained + } + } finally { + processStub.removeAllListeners() + } +} + +const results = { + node: process.version, + platform: process.platform, + architecture: process.arch, + heapLimitBytes: getHeapStatistics().heap_size_limit, + sourcePath, + baselineTransform: 'Remove only the three-line pending-membership guard in memory', + harness: 'Production entry in isolated VM contexts; imported collaborators stubbed', + before: await run('before'), + after: await run('after') +} +process.stdout.write(`${JSON.stringify(results, null, 2)}\n`) diff --git a/docs/audits/scanner-late-cancel/results.json b/docs/audits/scanner-late-cancel/results.json new file mode 100644 index 00000000000..71df02a9ccd --- /dev/null +++ b/docs/audits/scanner-late-cancel/results.json @@ -0,0 +1,27 @@ +{ + "node": "v26.6.0", + "platform": "darwin", + "architecture": "arm64", + "heapLimitBytes": 234881024, + "sourcePath": "src/main/ai-vault/session-scanner-service-entry.ts", + "baselineTransform": "Remove only the three-line pending-membership guard in memory", + "harness": "Production entry in isolated VM contexts; imported collaborators stubbed", + "before": { + "source": "working tree without pending guard", + "sourceSha256": "846f9af9577dbaf14d05aa5a2eea9e16a44a28ef7993e6b04c43b65db1f1f44f", + "requests": 1000, + "responses": 1000, + "pending": 0, + "controllers": 0, + "cancelled": 1000 + }, + "after": { + "source": "working tree", + "sourceSha256": "e300d4922da94da09abb3c14bbb240ef9593d8a4834a37598c1c2f77b14dcbf5", + "requests": 1000, + "responses": 1000, + "pending": 0, + "controllers": 0, + "cancelled": 0 + } +} diff --git a/src/main/ai-vault/session-scanner-service-cancellation.test.ts b/src/main/ai-vault/session-scanner-service-cancellation.test.ts new file mode 100644 index 00000000000..8863006017e --- /dev/null +++ b/src/main/ai-vault/session-scanner-service-cancellation.test.ts @@ -0,0 +1,187 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { + AiVaultServiceChildMessage, + AiVaultServiceParentMessage +} from './session-scanner-service-protocol' +import { AI_VAULT_SERVICE_PROTOCOL_VERSION } from './session-scanner-service-protocol' + +const scanAiVaultSessions = vi.hoisted(() => vi.fn()) +const flushSessionParseCachePersist = vi.hoisted(() => vi.fn(async () => undefined)) +const closeSearch = vi.hoisted(() => vi.fn()) + +vi.mock('./session-scanner', () => ({ scanAiVaultSessions })) +vi.mock('./session-scanner-parse-cache', () => ({ invalidateSessionParseCacheEntry: vi.fn() })) +vi.mock('./session-parse-cache-persistence', () => ({ + flushSessionParseCachePersist, + initSessionParseCachePersistence: vi.fn() +})) +vi.mock('./session-scanner-service-search', () => ({ + SessionScannerServiceSearch: class { + handles(): boolean { + return false + } + close(): void { + closeSearch() + } + } +})) + +const result = { sessions: [], issues: [], scannedAt: '2026-09-15' } +const sent: AiVaultServiceChildMessage[] = [] +const disconnect = vi.fn() +let restoreProcess = (): void => undefined + +function emit(message: AiVaultServiceParentMessage): void { + process.emit('message', message) +} + +function scan(id: number): void { + emit({ type: 'request', id, operation: 'scan', options: {} }) +} + +function settle(): Promise { + return new Promise((resolve) => setImmediate(resolve)) +} + +function cancelAndObserveSet(id: number): Set { + const add = vi.spyOn(Set.prototype, 'add') + try { + emit({ type: 'cancel', id }) + const index = add.mock.calls.findIndex(([value]) => value === id) + const retained = add.mock.contexts[index] + if (!(retained instanceof Set)) { + throw new Error('Expected an admitted cancellation.') + } + return retained + } finally { + add.mockRestore() + } +} + +beforeEach(async () => { + vi.resetModules() + vi.clearAllMocks() + sent.length = 0 + scanAiVaultSessions.mockResolvedValue(result) + const sendDescriptor = Object.getOwnPropertyDescriptor(process, 'send') + const disconnectDescriptor = Object.getOwnPropertyDescriptor(process, 'disconnect') + const messageListeners = new Set(process.listeners('message')) + const disconnectListeners = new Set(process.listeners('disconnect')) + Object.defineProperty(process, 'send', { + configurable: true, + value: (message: AiVaultServiceChildMessage) => { + sent.push(message) + return true + } + }) + Object.defineProperty(process, 'disconnect', { configurable: true, value: disconnect }) + restoreProcess = () => { + for (const listener of process.listeners('message')) { + if (!messageListeners.has(listener)) { + process.removeListener('message', listener) + } + } + for (const listener of process.listeners('disconnect')) { + if (!disconnectListeners.has(listener)) { + process.removeListener('disconnect', listener) + } + } + if (sendDescriptor) { + Object.defineProperty(process, 'send', sendDescriptor) + } else { + Reflect.deleteProperty(process, 'send') + } + if (disconnectDescriptor) { + Object.defineProperty(process, 'disconnect', disconnectDescriptor) + } else { + Reflect.deleteProperty(process, 'disconnect') + } + } + await import('./session-scanner-service-entry') + emit({ + type: 'init', + protocol: AI_VAULT_SERVICE_PROTOCOL_VERSION, + sessionParseCache: null, + sessionSearch: null + }) +}) + +afterEach(async () => { + emit({ type: 'shutdown' }) + await settle() + restoreProcess() +}) + +describe('AI Vault service cancellation ownership', () => { + it.each(['result', 'error'] as const)( + 'does not retain late cancellations after a %s response', + async (responseType) => { + const first = Promise.withResolvers() + scanAiVaultSessions.mockReturnValueOnce(first.promise) + scan(1) + await settle() + const cancelled = cancelAndObserveSet(1) + first.resolve(result) + await settle() + expect(cancelled.size).toBe(0) + + if (responseType === 'error') { + scanAiVaultSessions.mockRejectedValue(new Error('Synthetic parse failure')) + } + for (let id = 2; id <= 65; id++) { + scan(id) + await settle() + expect(sent).toContainEqual(expect.objectContaining({ type: responseType, id })) + // The parent can cancel while this completed response is still in transit. + emit({ type: 'cancel', id }) + } + emit({ type: 'cancel', id: 999 }) + expect(cancelled.size).toBe(0) + } + ) + + it('cancels running and queued requests and releases both IDs when they settle', async () => { + const first = Promise.withResolvers() + const signals: AbortSignal[] = [] + scanAiVaultSessions.mockImplementation(({ signal }: { signal: AbortSignal }) => { + signals.push(signal) + return signals.length === 1 ? first.promise : Promise.resolve(result) + }) + scan(1) + scan(2) + await settle() + const cancelled = cancelAndObserveSet(1) + emit({ type: 'cancel', id: 2 }) + expect(signals).toHaveLength(1) + expect(signals[0]?.aborted).toBe(true) + expect(cancelled.size).toBe(2) + first.resolve(result) + await settle() + expect(signals).toHaveLength(2) + expect(signals[1]?.aborted).toBe(true) + expect(cancelled.size).toBe(0) + expect(sent.filter((message) => message.type === 'result')).toHaveLength(2) + }) + + it('aborts the running request and closes the service before ignoring later cancels', async () => { + const first = Promise.withResolvers() + let signal: AbortSignal | undefined + scanAiVaultSessions.mockImplementation((options: { signal: AbortSignal }) => { + signal = options.signal + return first.promise + }) + scan(1) + await settle() + const cancelled = cancelAndObserveSet(1) + emit({ type: 'shutdown' }) + expect(signal?.aborted).toBe(true) + first.resolve(result) + await settle() + expect(cancelled.size).toBe(0) + expect(closeSearch).toHaveBeenCalledOnce() + expect(flushSessionParseCachePersist).toHaveBeenCalledOnce() + expect(disconnect).toHaveBeenCalledOnce() + emit({ type: 'cancel', id: 1 }) + expect(cancelled.size).toBe(0) + }) +}) diff --git a/src/main/ai-vault/session-scanner-service-entry.ts b/src/main/ai-vault/session-scanner-service-entry.ts index 7458db74e3a..d1a9da14a61 100644 --- a/src/main/ai-vault/session-scanner-service-entry.ts +++ b/src/main/ai-vault/session-scanner-service-entry.ts @@ -187,6 +187,9 @@ process.on('message', (raw: AiVaultServiceParentMessage) => { return } if (raw?.type === 'cancel') { + if (!pending.has(raw.id)) { + return + } cancelled.add(raw.id) controllers.get(raw.id)?.abort() return From bdad0e0f00325e6242fb6240d6aaa3120d2bbaa0 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:11:58 -0700 Subject: [PATCH 09/59] fix(browser): release page callbacks when a guest is destroyed (#21010) * fix(browser): release page callbacks when a guest is destroyed * fix: address memory PR review regressions and withdraw false positives --------- Co-authored-by: m4air --- .../README.md | 40 +++ .../reproduce.mjs | 168 +++++++++ .../results.json | 321 ++++++++++++++++++ ...-manager-destroyed-guest-downloads.test.ts | 152 +++++++++ ...er-manager-destroyed-guest-test-fixture.ts | 69 ++++ .../browser-manager-destroyed-guest.test.ts | 148 ++++++++ .../browser-manager-download-lifecycle.ts | 31 ++ ...browser-manager-guest-navigation-policy.ts | 7 +- .../browser/browser-manager-registration.ts | 18 +- src/main/browser/browser-manager-state.ts | 5 +- 10 files changed, 953 insertions(+), 6 deletions(-) create mode 100644 docs/audits/browser-destroyed-guest-retention/README.md create mode 100644 docs/audits/browser-destroyed-guest-retention/reproduce.mjs create mode 100644 docs/audits/browser-destroyed-guest-retention/results.json create mode 100644 src/main/browser/browser-manager-destroyed-guest-downloads.test.ts create mode 100644 src/main/browser/browser-manager-destroyed-guest-test-fixture.ts create mode 100644 src/main/browser/browser-manager-destroyed-guest.test.ts diff --git a/docs/audits/browser-destroyed-guest-retention/README.md b/docs/audits/browser-destroyed-guest-retention/README.md new file mode 100644 index 00000000000..d07a30dc389 --- /dev/null +++ b/docs/audits/browser-destroyed-guest-retention/README.md @@ -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. diff --git a/docs/audits/browser-destroyed-guest-retention/reproduce.mjs b/docs/audits/browser-destroyed-guest-retention/reproduce.mjs new file mode 100644 index 00000000000..f1dd27e8f04 --- /dev/null +++ b/docs/audits/browser-destroyed-guest-retention/reproduce.mjs @@ -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 }) +} diff --git a/docs/audits/browser-destroyed-guest-retention/results.json b/docs/audits/browser-destroyed-guest-retention/results.json new file mode 100644 index 00000000000..868b812529e --- /dev/null +++ b/docs/audits/browser-destroyed-guest-retention/results.json @@ -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 +} diff --git a/src/main/browser/browser-manager-destroyed-guest-downloads.test.ts b/src/main/browser/browser-manager-destroyed-guest-downloads.test.ts new file mode 100644 index 00000000000..28b228d6f43 --- /dev/null +++ b/src/main/browser/browser-manager-destroyed-guest-downloads.test.ts @@ -0,0 +1,152 @@ +import { afterEach, beforeEach, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ contents: new Map() })) +vi.mock('electron', () => ({ + app: { getPath: () => '/downloads' }, + BrowserWindow: { fromWebContents: () => null }, + clipboard: { writeText: vi.fn() }, + shell: { openExternal: vi.fn() }, + Menu: { buildFromTemplate: vi.fn() }, + screen: { getCursorScreenPoint: () => ({ x: 0, y: 0 }) }, + webContents: { fromId: (id: number) => mocks.contents.get(id) ?? null } +})) +vi.mock('./popup-origin-bar-window', () => ({ openPopupWithOriginBar: vi.fn() })) + +import { + DestroyedGuestTestContents, + DestroyedGuestTestManager +} from './browser-manager-destroyed-guest-test-fixture' +import { createDownloadItem, getDownloadItemEventHandler } from './browser-manager-test-harness' + +const event = { preventDefault: () => {}, defaultPrevented: false } +const manager = new DestroyedGuestTestManager() +const renderer = { isDestroyed: vi.fn(() => false), send: vi.fn() } +let nextGuestId = 1 + +function register(rendererId = 5001): DestroyedGuestTestContents { + const guest = new DestroyedGuestTestContents(nextGuestId++) + mocks.contents.set(guest.id, guest.asWebContents()) + mocks.contents.set(rendererId, renderer) + manager.attachGuestPolicies(guest.asWebContents()) + expect( + manager.registerGuest({ + browserPageId: 'recoverable-page', + webContentsId: guest.id, + rendererWebContentsId: rendererId + }) + ).toBe(true) + return guest +} + +function download(guest: DestroyedGuestTestContents): Electron.DownloadItem { + const item = createDownloadItem() + manager.handleGuestWillDownload({ guestWebContentsId: guest.id, item }) + return item +} + +beforeEach(() => { + renderer.isDestroyed.mockReset().mockReturnValue(false) + renderer.send.mockClear() +}) +afterEach(() => { + manager.unregisterAll() + mocks.contents.clear() +}) + +it.each(['completed', 'cancelled', 'interrupted'] as const)( + 'preserves page download until native %s and then releases its routing entry', + (state) => { + const guest = register() + const item = download(guest) + guest.destroy() + expect(item.cancel).not.toHaveBeenCalled() + expect(manager.retainedCounts()).toMatchObject({ guests: 0, contextMenus: 0, renderers: 1 }) + getDownloadItemEventHandler(item, 'on', 'updated')?.(event, 'progressing') + expect(renderer.send).toHaveBeenCalledWith( + 'browser:download-progress', + expect.objectContaining({ browserPageId: 'recoverable-page' }) + ) + getDownloadItemEventHandler(item, 'once', 'done')?.(event, state) + expect(renderer.send).toHaveBeenCalledWith( + 'browser:download-finished', + expect.objectContaining({ + browserPageId: 'recoverable-page', + status: state === 'completed' ? 'completed' : state === 'cancelled' ? 'canceled' : 'failed' + }) + ) + expect(manager.downloadCount()).toBe(0) + expect(manager.retainedCounts().renderers).toBe(0) + } +) + +it('still cancels a download when its logical page closes after guest destruction', () => { + const guest = register() + const item = download(guest) + guest.destroy() + manager.unregisterGuest('recoverable-page') + expect(item.cancel).toHaveBeenCalledOnce() + expect(renderer.send).toHaveBeenCalledWith( + 'browser:download-finished', + expect.objectContaining({ status: 'canceled', error: 'Tab closed before download completed.' }) + ) + expect(manager.downloadCount()).toBe(0) + expect(manager.retainedCounts().renderers).toBe(0) +}) + +it('keeps progress routing until the last of multiple downloads settles', () => { + const guest = register() + const first = download(guest) + const second = download(guest) + guest.destroy() + getDownloadItemEventHandler(first, 'once', 'done')?.(event, 'completed') + expect(manager.downloadCount()).toBe(1) + expect(manager.retainedCounts().renderers).toBe(1) + getDownloadItemEventHandler(second, 'once', 'done')?.(event, 'completed') + expect(manager.downloadCount()).toBe(0) + expect(manager.retainedCounts().renderers).toBe(0) +}) + +it('keeps the replacement guest and its routing when an old download settles', () => { + const old = register() + const item = download(old) + old.destroy() + const replacement = register(5002) + getDownloadItemEventHandler(item, 'once', 'done')?.(event, 'completed') + expect(manager.getGuestWebContentsId('recoverable-page')).toBe(replacement.id) + expect(manager.retainedCounts().renderers).toBe(1) + expect(manager.downloadCount()).toBe(0) +}) + +it('releases current routing after two guest destructions and the final old download settles', () => { + const old = register() + const item = download(old) + old.destroy() + register(5002).destroy() + getDownloadItemEventHandler(item, 'once', 'done')?.(event, 'completed') + expect(manager.downloadCount()).toBe(0) + expect(manager.retainedCounts().renderers).toBe(0) +}) + +it('does not erase a replacement routing owner installed during completion delivery', () => { + const old = register() + const item = download(old) + old.destroy() + renderer.send.mockImplementationOnce(() => register(5002)) + getDownloadItemEventHandler(item, 'once', 'done')?.(event, 'completed') + expect(manager.retainedCounts().renderers).toBe(1) + manager.unregisterGuest('recoverable-page') + expect(manager.retainedCounts().renderers).toBe(0) +}) + +it('releases download state after renderer loss without sending to a destroyed renderer', () => { + const guest = register() + const item = download(guest) + guest.destroy() + renderer.isDestroyed.mockReturnValue(true) + renderer.send.mockClear() + getDownloadItemEventHandler(item, 'on', 'updated')?.(event, 'progressing') + getDownloadItemEventHandler(item, 'once', 'done')?.(event, 'completed') + expect(renderer.send).not.toHaveBeenCalled() + expect(manager.downloadCount()).toBe(0) + expect(manager.retainedCounts().renderers).toBe(0) +}) diff --git a/src/main/browser/browser-manager-destroyed-guest-test-fixture.ts b/src/main/browser/browser-manager-destroyed-guest-test-fixture.ts new file mode 100644 index 00000000000..3542a6c8b7e --- /dev/null +++ b/src/main/browser/browser-manager-destroyed-guest-test-fixture.ts @@ -0,0 +1,69 @@ +import { EventEmitter } from 'node:events' +import type { WebContents } from 'electron' +import { BrowserManager } from './browser-manager' + +const session = { getUserAgent: () => 'Chrome/140.0.0.0' } + +export class DestroyedGuestTestManager extends BrowserManager { + downloadCount(): number { + return this.downloadsById.size + } + + retainedCounts(): Record { + return { + guests: this.webContentsIdByTabId.size, + contextMenus: this.contextMenuCleanupByTabId.size, + grabShortcuts: this.grabShortcutCleanupByTabId.size, + appShortcuts: this.shortcutForwardingCleanupByTabId.size, + wheelHandlers: this.mouseWheelZoomCleanupByTabId.size, + renderers: this.rendererWebContentsIdByTabId.size, + workspaces: this.workspaceIdByPageId.size, + worktrees: this.worktreeIdByTabId.size, + profiles: this.sessionProfileIdByPageId.size, + policies: this.policyCleanupByGuestId.size + } + } +} + +export class DestroyedGuestTestContents extends EventEmitter { + readonly debugger = Object.assign(new EventEmitter(), { + isAttached: () => false, + sendCommand: async () => undefined + }) + readonly session = session + private destroyed = false + + constructor(readonly id: number) { + super() + } + + asWebContents(): WebContents { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture provides the WebContents methods exercised by guest registration and teardown. + return this as unknown as WebContents + } + + isDestroyed(): boolean { + return this.destroyed + } + + getType(): string { + return 'webview' + } + + getURL(): string { + return 'https://example.test' + } + + getUserAgent(): string { + return session.getUserAgent() + } + + setUserAgent(): void {} + setWindowOpenHandler(): void {} + setBackgroundThrottling(): void {} + + destroy(): void { + this.destroyed = true + this.emit('destroyed') + } +} diff --git a/src/main/browser/browser-manager-destroyed-guest.test.ts b/src/main/browser/browser-manager-destroyed-guest.test.ts new file mode 100644 index 00000000000..faacbb56fc4 --- /dev/null +++ b/src/main/browser/browser-manager-destroyed-guest.test.ts @@ -0,0 +1,148 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ guests: new Map() })) +vi.mock('electron', () => ({ + app: { getPath: () => '/downloads' }, + BrowserWindow: { fromWebContents: () => null }, + clipboard: { writeText: vi.fn() }, + shell: { openExternal: vi.fn() }, + Menu: { buildFromTemplate: vi.fn() }, + screen: { getCursorScreenPoint: () => ({ x: 0, y: 0 }) }, + webContents: { fromId: (id: number) => mocks.guests.get(id) ?? null } +})) +vi.mock('./popup-origin-bar-window', () => ({ openPopupWithOriginBar: vi.fn() })) + +import { + DestroyedGuestTestContents, + DestroyedGuestTestManager +} from './browser-manager-destroyed-guest-test-fixture' + +describe('browser guest destruction ownership', () => { + const manager = new DestroyedGuestTestManager() + const pageIds = new Set() + const guests = new Set() + let nextId = 1 + + function createGuest(): DestroyedGuestTestContents { + const guest = new DestroyedGuestTestContents(nextId++) + guests.add(guest) + mocks.guests.set(guest.id, guest.asWebContents()) + return guest + } + + function register(pageId: string): DestroyedGuestTestContents { + const guest = createGuest() + pageIds.add(pageId) + manager.attachGuestPolicies(guest.asWebContents()) + expect( + manager.registerGuest({ + browserPageId: pageId, + webContentsId: guest.id, + rendererWebContentsId: 5001, + workspaceId: 'workspace-1', + worktreeId: 'worktree-1', + sessionProfileId: 'profile-1' + }) + ).toBe(true) + return guest + } + + function expectRetainedCount(count: number): void { + const counts = manager.retainedCounts() + expect(counts).toEqual(Object.fromEntries(Object.keys(counts).map((key) => [key, count]))) + } + + afterEach(() => { + for (const pageId of pageIds) { + manager.unregisterGuest(pageId) + } + manager.unregisterAll() + for (const guest of guests) { + guest.removeAllListeners() + } + pageIds.clear() + guests.clear() + mocks.guests.clear() + }) + + it('releases registered callbacks and ownership after 1000 distinct guest destructions', () => { + for (let index = 0; index < 1000; index++) { + register(`retired-${index}`).destroy() + } + expectRetainedCount(0) + }) + + it('leaves no dead-guest callbacks for the window-close unregisterAll path', () => { + for (let index = 0; index < 1000; index++) { + register(`window-${index}`).destroy() + } + manager.unregisterAll() + expectRetainedCount(0) + }) + + it('keeps explicit unregister before destruction idempotent', () => { + const guest = register('explicit') + manager.unregisterGuest('explicit') + guest.destroy() + manager.unregisterGuest('explicit') + expectRetainedCount(0) + }) + + it('does not let a captured old destroyed callback retire a replacement guest', () => { + const old = register('replacement') + const [oldDestroyed] = old.listeners('destroyed') + expect(oldDestroyed).toBeTypeOf('function') + const replacement = register('replacement') + oldDestroyed.call(old) + expect(manager.getGuestWebContentsId('replacement')).toBe(replacement.id) + expect(manager.getWorktreeIdForTab('replacement')).toBe('worktree-1') + expectRetainedCount(1) + }) + + it('cleans a popup without retiring its live primary page', () => { + const parent = register('popup-parent') + const popup = createGuest() + manager.attachGuestPolicies(popup.asWebContents(), { + rootGuestWebContentsId: parent.id, + browserTabId: 'popup-parent' + }) + popup.destroy() + expect(manager.getGuestWebContentsId('popup-parent')).toBe(parent.id) + expectRetainedCount(1) + }) + + it('cleans policies for a guest destroyed before registration', () => { + const guest = createGuest() + manager.attachGuestPolicies(guest.asWebContents()) + guest.destroy() + expectRetainedCount(0) + }) + + it('preserves live guest ownership when its renderer process needs reload recovery', () => { + const guest = register('renderer-recovery') + guest.emit('render-process-gone', {}, { reason: 'crashed', exitCode: 1 }) + expect(manager.getGuestWebContentsId('renderer-recovery')).toBe(guest.id) + expect(manager.getSessionProfileIdForTab('renderer-recovery')).toBe('profile-1') + expectRetainedCount(1) + }) + + it('rebuilds ownership when a restored page registers its fresh guest', () => { + register('fresh-owner').destroy() + const replacement = register('fresh-owner') + expect(manager.getGuestWebContentsId('fresh-owner')).toBe(replacement.id) + expect(manager.getWorktreeIdForTab('fresh-owner')).toBe('worktree-1') + expect(manager.getSessionProfileIdForTab('fresh-owner')).toBe('profile-1') + expectRetainedCount(1) + }) + + it('preserves a sibling page using the same browser session profile', () => { + const retiring = register('retiring') + const sibling = register('sibling') + expect(retiring.session).toBe(sibling.session) + retiring.destroy() + expect(manager.getGuestWebContentsId('sibling')).toBe(sibling.id) + expect(manager.getSessionProfileIdForTab('sibling')).toBe('profile-1') + expect(manager.getWorktreeIdForTab('sibling')).toBe('worktree-1') + expectRetainedCount(1) + }) +}) diff --git a/src/main/browser/browser-manager-download-lifecycle.ts b/src/main/browser/browser-manager-download-lifecycle.ts index 614b7651d16..ba45eed1d0d 100644 --- a/src/main/browser/browser-manager-download-lifecycle.ts +++ b/src/main/browser/browser-manager-download-lifecycle.ts @@ -34,6 +34,9 @@ export abstract class BrowserManagerDownloadLifecycle extends BrowserManagerDown if (!download) { return } + const rendererOwner = download.browserTabId + ? this.rendererWebContentsIdByTabId.get(download.browserTabId) + : undefined this.sendDownloadStarted(downloadId) if (download.receivedBytes > 0 || download.transientState) { this.sendDownloadProgress(download.browserTabId, { @@ -50,6 +53,7 @@ export abstract class BrowserManagerDownloadLifecycle extends BrowserManagerDown browserPageId: download.browserTabId ?? undefined }) this.downloadsById.delete(downloadId) + this.releaseRetiredDownloadRenderer(download.browserTabId, rendererOwner) } } @@ -150,6 +154,9 @@ export abstract class BrowserManagerDownloadLifecycle extends BrowserManagerDown if (!download) { return } + const rendererOwner = download.browserTabId + ? this.rendererWebContentsIdByTabId.get(download.browserTabId) + : undefined if (download.cleanup) { download.cleanup() @@ -169,6 +176,7 @@ export abstract class BrowserManagerDownloadLifecycle extends BrowserManagerDown } this.downloadsById.delete(downloadId) + this.releaseRetiredDownloadRenderer(download.browserTabId, rendererOwner) } protected finishDownloadInternal( @@ -180,6 +188,9 @@ export abstract class BrowserManagerDownloadLifecycle extends BrowserManagerDown if (!download || download.terminalEvent) { return } + const rendererOwner = download.browserTabId + ? this.rendererWebContentsIdByTabId.get(download.browserTabId) + : undefined if (download.cleanup) { download.cleanup() @@ -205,9 +216,29 @@ export abstract class BrowserManagerDownloadLifecycle extends BrowserManagerDown this.sendDownloadStarted(downloadId) this.sendDownloadFinished(download.browserTabId, event) this.downloadsById.delete(downloadId) + this.releaseRetiredDownloadRenderer(download.browserTabId, rendererOwner) } } + private releaseRetiredDownloadRenderer( + browserTabId: string | null, + rendererOwner: number | undefined + ): void { + if ( + !browserTabId || + this.webContentsIdByTabId.has(browserTabId) || + this.rendererWebContentsIdByTabId.get(browserTabId) !== rendererOwner + ) { + return + } + for (const download of this.downloadsById.values()) { + if (download.browserTabId === browserTabId) { + return + } + } + this.rendererWebContentsIdByTabId.delete(browserTabId) + } + protected cancelPendingDownloadsForGuest(guestWebContentsId: number): void { const pending = this.pendingDownloadIdsByGuestId.get(guestWebContentsId) this.pendingDownloadIdsByGuestId.delete(guestWebContentsId) diff --git a/src/main/browser/browser-manager-guest-navigation-policy.ts b/src/main/browser/browser-manager-guest-navigation-policy.ts index abacd268640..dbe54a8ba13 100644 --- a/src/main/browser/browser-manager-guest-navigation-policy.ts +++ b/src/main/browser/browser-manager-guest-navigation-policy.ts @@ -129,7 +129,12 @@ export abstract class BrowserManagerGuestNavigationPolicy extends BrowserManager guest.on('did-navigate', didNavigateHandler) guest.on('did-fail-load', didFailLoadHandler) const handleDestroyed = (): void => { - // Why: guests can die before renderer registration, else attach-time closures leak until shutdown. + 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 + } this.cleanupGuestPolicyAttachment(guest.id) } guest.on('destroyed', handleDestroyed) diff --git a/src/main/browser/browser-manager-registration.ts b/src/main/browser/browser-manager-registration.ts index 4f6abbe67e9..816c0d61cb2 100644 --- a/src/main/browser/browser-manager-registration.ts +++ b/src/main/browser/browser-manager-registration.ts @@ -72,7 +72,10 @@ export abstract class BrowserManagerRegistration extends BrowserManagerGuestPoli return true } - unregisterGuest(browserTabId: string): void { + unregisterGuest( + browserTabId: string, + reason: 'page-closed' | 'guest-destroyed' = 'page-closed' + ): void { // Why the check on the exit door too: a document page withdraws by revoking its grant, never // through here, so its id arriving is misaddressed — and the cancel below would evict that // preview's live grab on the strength of it. @@ -108,10 +111,14 @@ export abstract class BrowserManagerRegistration extends BrowserManagerGuestPoli mouseWheelZoomCleanup() this.mouseWheelZoomCleanupByTabId.delete(browserTabId) } - // Why: downloads are per-tab chrome; closing the tab must cancel active writes, not orphan them. + let hasActiveDownloads = false for (const [downloadId, download] of this.downloadsById.entries()) { if (download.browserTabId === browserTabId && !download.terminalEvent) { - this.cancelDownloadInternal(downloadId, 'Tab closed before download completed.') + if (reason === 'page-closed') { + this.cancelDownloadInternal(downloadId, 'Tab closed before download completed.') + } else { + hasActiveDownloads = true + } } } const wcId = this.webContentsIdByTabId.get(browserTabId) @@ -119,7 +126,10 @@ export abstract class BrowserManagerRegistration extends BrowserManagerGuestPoli this.tabIdByWebContentsId.delete(wcId) } this.webContentsIdByTabId.delete(browserTabId) - this.rendererWebContentsIdByTabId.delete(browserTabId) + // A destroyed guest can recover in an open page while its downloads still report progress. + if (!hasActiveDownloads) { + this.rendererWebContentsIdByTabId.delete(browserTabId) + } this.workspaceIdByPageId.delete(browserTabId) this.sessionProfileIdByPageId.delete(browserTabId) this.worktreeIdByTabId.delete(browserTabId) diff --git a/src/main/browser/browser-manager-state.ts b/src/main/browser/browser-manager-state.ts index c30c5ae8f17..da7fa256b4e 100644 --- a/src/main/browser/browser-manager-state.ts +++ b/src/main/browser/browser-manager-state.ts @@ -75,7 +75,10 @@ export abstract class BrowserManagerState extends BrowserManagerViewportScrollSt ): void protected abstract cancelGrabOp(browserTabId: string, reason: BrowserGrabCancelReason): void protected abstract hasActiveGrabOp(browserTabId: string): boolean - protected abstract unregisterGuest(browserTabId: string): void + protected abstract unregisterGuest( + browserTabId: string, + reason?: 'page-closed' | 'guest-destroyed' + ): void protected abstract cancelDownloadInternal(downloadId: string, reason: string): void protected abstract bindDownloadToTab(downloadId: string, browserTabId: string): void protected abstract flushDownloadSnapshot(downloadId: string): void From 0e3acf577d7285057e039d99d2891799dbdc3fbb Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:02 -0700 Subject: [PATCH 10/59] fix: release consumed runtime RPC queue entries (#21131) Co-authored-by: m4air --- .../runtime-rpc-consumed-queue/README.md | 47 ++++ .../baseline.config.mjs | 30 +++ .../electron-extended-results.json | 108 ++++++++ .../electron-resolver-results.json | 10 + .../electron-results.json | 152 ++++++++++++ .../extended-controls.cjs | 230 ++++++++++++++++++ .../extended-results.json | 108 ++++++++ .../runtime-rpc-consumed-queue/fix.patch | 42 ++++ .../queue-source.cjs | 52 ++++ .../runtime-rpc-consumed-queue/reproduce.cjs | 109 +++++++++ .../resolver-controls.cjs | 55 +++++ .../resolver-results.json | 10 + .../runtime-rpc-consumed-queue/results.json | 152 ++++++++++++ .../source-versions.json | 9 + .../runtime-rpc-call-queue-retention.test.ts | 183 ++++++++++++++ src/shared/runtime-rpc-call-queue.ts | 9 +- 16 files changed, 1302 insertions(+), 4 deletions(-) create mode 100644 docs/audits/runtime-rpc-consumed-queue/README.md create mode 100644 docs/audits/runtime-rpc-consumed-queue/baseline.config.mjs create mode 100644 docs/audits/runtime-rpc-consumed-queue/electron-extended-results.json create mode 100644 docs/audits/runtime-rpc-consumed-queue/electron-resolver-results.json create mode 100644 docs/audits/runtime-rpc-consumed-queue/electron-results.json create mode 100644 docs/audits/runtime-rpc-consumed-queue/extended-controls.cjs create mode 100644 docs/audits/runtime-rpc-consumed-queue/extended-results.json create mode 100644 docs/audits/runtime-rpc-consumed-queue/fix.patch create mode 100644 docs/audits/runtime-rpc-consumed-queue/queue-source.cjs create mode 100644 docs/audits/runtime-rpc-consumed-queue/reproduce.cjs create mode 100644 docs/audits/runtime-rpc-consumed-queue/resolver-controls.cjs create mode 100644 docs/audits/runtime-rpc-consumed-queue/resolver-results.json create mode 100644 docs/audits/runtime-rpc-consumed-queue/results.json create mode 100644 docs/audits/runtime-rpc-consumed-queue/source-versions.json create mode 100644 src/shared/runtime-rpc-call-queue-retention.test.ts diff --git a/docs/audits/runtime-rpc-consumed-queue/README.md b/docs/audits/runtime-rpc-consumed-queue/README.md new file mode 100644 index 00000000000..69404df7a5e --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/README.md @@ -0,0 +1,47 @@ +# Completed RPC queue records remain reachable + +Status: reproduced before and after the product fix on Node 26 and installed Electron 43.7.0 in Node mode. + +`RuntimeRpcCallQueuePool` advances each lane's head without clearing the consumed array element. While any call keeps that selector active, completed records keep their `run`, `resolve`, `reject`, and signal fields reachable until that same lane compacts. Compaction requires more than 32 consumed entries and at least half the array consumed. Selector deletion also releases the arrays when every active and queued call finishes. + +The fix assigns `undefined` to the consumed slot in `takeForeground` and `takeBackground` and permits undefined in the two array element types. An active call remains owned by its promise cleanup closure. Queued entries, both lane heads, counts, admission thresholds, batching, foreground preference, and cancellation behavior stay intact. + +## Production reachability + +- Desktop `src/main/ipc/runtime-environment-call-queue.ts` owns a module singleton. `runtime-environment-transport-routing.ts::callRuntimeEnvironment` passes a closure capturing request params, environment, and optional orchestration envelope. Its normal timeout is 15 seconds; `status.get` bypasses this queue. +- Paired web `src/renderer/src/web/preload-api/web-runtime-session.ts` owns another module singleton. `web-runtime-calls.ts` and `web-filesystem-api.ts::captureWebFileMutationSession` pass closures capturing params, environment, and sometimes an explicit client. Web request timeout defaults to 30 seconds after connection readiness. +- Current production callers pass/default `retainedBytes` to zero. The nonzero byte values in the proof exercise the queue's accounting; they are not evidence that deployed callers account their object graphs here. +- These are remote/paired execution paths. The finding does not explain local-only #19831 from code reachability alone. + +Finite individual call duration does not guarantee that an old lane's consumed records disappear: overlapping calls in the other lane can keep the selector active. The extended proof completes 70 successive background calls while eight completed foreground payloads stay reachable before the fix; each background call finishes, and releasing the final call makes the selector idle and permits collection. + +This is retention beyond useful lifetime, bounded in record count by existing compaction/admission behavior. It is not proof of unlimited growth for one selector or of any reported incident's magnitude. The isolated payloads are bounded dummy arrays rather than a real network workload. + +## Proofs + +`reproduce.cjs` bundles the actual queue module with its actual imports. `queue-source.cjs` reconstructs the baseline in memory by reversing `fix.patch`; both baseline and current source hashes must match `source-versions.json`. Eight completed calls capture eight 1 MiB typed-array payloads, with one other call holding the selector active. Weak references remain live before and all clear after the fix. Foreground and background lanes, eventual compaction, and eventual idle cleanup are covered. Queue byte credit and queued-call count already equal zero during stale retention. At most eight payload MiB are intentionally live in each fixture; the process has a 128 MiB old-space limit and a ten-second deadline. + +`extended-controls.cjs` checks active payloads are retained until their call settles, cancellation releases queued payloads without waiting for unrelated active calls, synchronous failure releases only after compaction/idle before the fix, rolling cross-lane traffic, and a 140-call mixed burst with six cancellations. Both variants execute the same 134 remaining calls in FIFO lane order with foreground priority, then delete the idle selector. + +`resolver-controls.cjs` isolates the runtime's settled-promise resolver behavior without using the queue. Keeping native resolve functions can also keep settled results reachable in some runtime versions; this must be reported separately from input closures. + +Node and Electron results are stored separately. Node 26.6.0/V8 14.6 collects fresh response payloads even with the stale queue records. Installed Electron 43.7.0/Node 24.21.0/V8 15.0 retains all eight fresh response payloads before and releases them after the fix. The standalone resolver control reproduces the same difference: saving native resolve functions retains eight of eight payloads in Electron and zero in Node; releasing those functions permits collection in both. This control isolates runtime promise behavior without claiming all Electron versions or browser renderer modes behave identically. + +The original Node-only negative-control expectation for response retention failed under Electron. The baseline now records that result rather than assuming every V8 version releases settled results identically. The fixed variant must release responses in both environments. This proof does not measure the exact Electron 43.4.1 historical binary. + +Run from the worktree: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=128 docs/audits/runtime-rpc-consumed-queue/reproduce.cjs +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=128 docs/audits/runtime-rpc-consumed-queue/extended-controls.cjs +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=128 docs/audits/runtime-rpc-consumed-queue/resolver-controls.cjs +ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/shared/runtime-rpc-call-queue.test.ts src/shared/runtime-rpc-call-queue-retention.test.ts +``` + +The installed Electron binary can run the same scripts with `ELECTRON_RUN_AS_NODE=1`, `ORCA_BACKGROUND_LAUNCH=1`, and the same Node flags. No Electron app or window is created. No network, microphone, or affected-host data is used. No heap-snapshot tools are exposed in this session; the proof measures WeakRef reachability and bounded process counters instead of reading raw heap snapshots. + +The existing eight queue tests and six added retention/lifecycle tests pass with the fix. To reproduce the three retention failures against the reconstructed baseline, use `ORCA_BACKGROUND_LAUNCH=1 node --expose-gc node_modules/vitest/vitest.mjs run --config docs/audits/runtime-rpc-consumed-queue/baseline.config.mjs src/shared/runtime-rpc-call-queue.test.ts src/shared/runtime-rpc-call-queue-retention.test.ts`; the expected outcome is 11 passing tests and three failures, with exit code 1. Node and Web project typechecks and the changed-code quality gate passed during promotion. Explicit basic/type-aware lint also covers these otherwise ignored audit scripts. + +## Source identity + +The queue source before the fix, fetched `origin/main`, and `v1.4.198` all had SHA-256 `45921658a10ffeefe0247673227aab643d1b5251d24aa460244fadd5480fc349` at review. `source-versions.json` records exact baseline/current hashes and compared commit IDs. Availability in that release supports reachability analysis; it does not attribute an incident. diff --git a/docs/audits/runtime-rpc-consumed-queue/baseline.config.mjs b/docs/audits/runtime-rpc-consumed-queue/baseline.config.mjs new file mode 100644 index 00000000000..07ca46c4336 --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/baseline.config.mjs @@ -0,0 +1,30 @@ +import path from 'node:path' +import { createRequire } from 'node:module' +import { defineConfig, mergeConfig } from 'vitest/config' +import rootConfig from '../../../config/vitest.config.ts' + +const require = createRequire(import.meta.url) +const proof = require('./queue-source.cjs') +const sourcePath = path.resolve(import.meta.dirname, '../../..', proof.versions.sourcePath) +export default mergeConfig( + rootConfig, + defineConfig({ + plugins: [ + { + name: 'runtime-rpc-queue-baseline', + enforce: 'pre', + load(id) { + if (id === sourcePath) { + return proof.baselineSource + } + } + } + ], + test: { + include: [ + 'src/shared/runtime-rpc-call-queue.test.ts', + 'src/shared/runtime-rpc-call-queue-retention.test.ts' + ] + } + }) +) diff --git a/docs/audits/runtime-rpc-consumed-queue/electron-extended-results.json b/docs/audits/runtime-rpc-consumed-queue/electron-extended-results.json new file mode 100644 index 00000000000..390bb34c318 --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/electron-extended-results.json @@ -0,0 +1,108 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "versions": { + "sourcePath": "src/shared/runtime-rpc-call-queue.ts", + "baselineSha256": "45921658a10ffeefe0247673227aab643d1b5251d24aa460244fadd5480fc349", + "fixedSha256": "6afe6eb8acd9025d227960fafef77ae205a455c7f680b15ad0db5cdfee18c71d", + "matchingBaselineRefs": { + "origin/main": "77cd61df396f25ec91ee2d5ddcbd1f55aa94f818", + "v1.4.198": "e0826956fcfc532f5a1e55b5e081f2e57e553c43" + } + }, + "proofSha256": "af64f8b55f070d4a863ff6b8f456d518d977cc06400bdbbbba60eb2c5a455aa2", + "reports": [ + { + "candidate": false, + "case": "active-and-cancelled", + "lane": "foreground", + "activeRetainedUntilSettlement": true, + "cancelledReleasedBeforeActiveFinishes": true + }, + { + "candidate": false, + "case": "synchronous-failure", + "lane": "foreground", + "retainedWhileOtherCallActive": 1, + "retainedAfterIdle": 0 + }, + { + "candidate": false, + "case": "active-and-cancelled", + "lane": "background", + "activeRetainedUntilSettlement": true, + "cancelledReleasedBeforeActiveFinishes": true + }, + { + "candidate": false, + "case": "synchronous-failure", + "lane": "background", + "retainedWhileOtherCallActive": 1, + "retainedAfterIdle": 0 + }, + { + "candidate": false, + "case": "rolling-background-traffic", + "completedForegroundPayloads": 8, + "completedBackgroundCalls": 70, + "foregroundPayloadsStillRetained": 8, + "retainedAfterIdle": 0, + "everyBackgroundCallCompletes": true + }, + { + "candidate": false, + "case": "ordering-compaction-and-cancellation", + "completed": 134, + "cancelled": 6, + "foregroundBeforeBackground": true, + "fifoWithinEachLane": true, + "finalQueueCount": 0 + }, + { + "candidate": true, + "case": "active-and-cancelled", + "lane": "foreground", + "activeRetainedUntilSettlement": true, + "cancelledReleasedBeforeActiveFinishes": true + }, + { + "candidate": true, + "case": "synchronous-failure", + "lane": "foreground", + "retainedWhileOtherCallActive": 0, + "retainedAfterIdle": 0 + }, + { + "candidate": true, + "case": "active-and-cancelled", + "lane": "background", + "activeRetainedUntilSettlement": true, + "cancelledReleasedBeforeActiveFinishes": true + }, + { + "candidate": true, + "case": "synchronous-failure", + "lane": "background", + "retainedWhileOtherCallActive": 0, + "retainedAfterIdle": 0 + }, + { + "candidate": true, + "case": "rolling-background-traffic", + "completedForegroundPayloads": 8, + "completedBackgroundCalls": 70, + "foregroundPayloadsStillRetained": 0, + "retainedAfterIdle": 0, + "everyBackgroundCallCompletes": true + }, + { + "candidate": true, + "case": "ordering-compaction-and-cancellation", + "completed": 134, + "cancelled": 6, + "foregroundBeforeBackground": true, + "fifoWithinEachLane": true, + "finalQueueCount": 0 + } + ] +} diff --git a/docs/audits/runtime-rpc-consumed-queue/electron-resolver-results.json b/docs/audits/runtime-rpc-consumed-queue/electron-resolver-results.json new file mode 100644 index 00000000000..9e568d55264 --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/electron-resolver-results.json @@ -0,0 +1,10 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "proofSha256": "725087b61b9d79bc2daff4fe45f52951c531dcf9d23137a4939508957848350e", + "payloads": 8, + "bytesPerPayload": 1048576, + "retainedWithResolveFunctions": 8, + "retainedAfterResolveFunctionsReleased": 0 +} diff --git a/docs/audits/runtime-rpc-consumed-queue/electron-results.json b/docs/audits/runtime-rpc-consumed-queue/electron-results.json new file mode 100644 index 00000000000..059aab505b5 --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/electron-results.json @@ -0,0 +1,152 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "versions": { + "sourcePath": "src/shared/runtime-rpc-call-queue.ts", + "baselineSha256": "45921658a10ffeefe0247673227aab643d1b5251d24aa460244fadd5480fc349", + "fixedSha256": "6afe6eb8acd9025d227960fafef77ae205a455c7f680b15ad0db5cdfee18c71d", + "matchingBaselineRefs": { + "origin/main": "77cd61df396f25ec91ee2d5ddcbd1f55aa94f818", + "v1.4.198": "e0826956fcfc532f5a1e55b5e081f2e57e553c43" + } + }, + "proofSha256": "79e415e5f7de6516b4e4bd8fc8ab0c6d0ac139efe996b8c39ef4cf7b8659bacb", + "reports": [ + { + "candidate": false, + "kind": "input", + "lane": "foreground", + "completed": 8, + "releaseByIdle": false, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 8, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 8378970, + "heapUsed": -303508 + } + }, + { + "candidate": false, + "kind": "input", + "lane": "background", + "completed": 8, + "releaseByIdle": false, + "historyLength": 8, + "head": 8, + "retainedBeforeCompaction": 8, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 8388608, + "heapUsed": -2300 + } + }, + { + "candidate": false, + "kind": "input", + "lane": "foreground", + "completed": 8, + "releaseByIdle": true, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 8, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 8388608, + "heapUsed": -860 + } + }, + { + "candidate": false, + "kind": "result", + "lane": "foreground", + "completed": 8, + "releaseByIdle": false, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 8, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 8388608, + "heapUsed": 14952 + } + }, + { + "candidate": true, + "kind": "input", + "lane": "foreground", + "completed": 8, + "releaseByIdle": false, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": -9748, + "heapUsed": -2608 + } + }, + { + "candidate": true, + "kind": "input", + "lane": "background", + "completed": 8, + "releaseByIdle": false, + "historyLength": 8, + "head": 8, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 0, + "heapUsed": 4176 + } + }, + { + "candidate": true, + "kind": "input", + "lane": "foreground", + "completed": 8, + "releaseByIdle": true, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 0, + "heapUsed": 2496 + } + }, + { + "candidate": true, + "kind": "result", + "lane": "foreground", + "completed": 8, + "releaseByIdle": false, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 0, + "heapUsed": -3112 + } + } + ] +} diff --git a/docs/audits/runtime-rpc-consumed-queue/extended-controls.cjs b/docs/audits/runtime-rpc-consumed-queue/extended-controls.cjs new file mode 100644 index 00000000000..0ad4ebd311a --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/extended-controls.cjs @@ -0,0 +1,230 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { load, collect, sha, versions } = require('./queue-source.cjs') + +const PAYLOAD_BYTES = 1024 * 1024 +function liveCall(queue, method = 'git.status') { + const hold = Promise.withResolvers() + return { + release: () => hold.resolve(), + settled: queue.enqueue('fixture', method, () => hold.promise) + } +} +function payloadCall(queue, method, hold, signal, throwSynchronously = false) { + const payload = new Uint8Array(PAYLOAD_BYTES) + payload[0] = 19 + const ref = new WeakRef(payload) + const settled = queue.enqueue( + 'fixture', + method, + () => { + if (throwSynchronously) { + throw new Error(`fixture failure ${payload[0]}`) + } + return hold.then(() => payload[0]) + }, + payload.byteLength, + signal + ) + return { ref, settled } +} +function liveCount(refs) { + return refs.filter((ref) => ref.deref() !== undefined).length +} + +async function checkActiveAndCancelled(Queue, candidate, lane) { + const queue = new Queue(1, 1) + const method = lane === 'foreground' ? 'terminal.send' : 'git.status' + const hold = Promise.withResolvers() + const active = payloadCall(queue, method, hold.promise) + const controller = new AbortController() + const queued = payloadCall(queue, method, Promise.resolve(), controller.signal) + const rejected = assert.rejects(queued.settled, { name: 'AbortError' }) + await collect() + assert.equal(liveCount([active.ref, queued.ref]), 2) + assert.equal(queue.retainedCallBytes, 2 * PAYLOAD_BYTES) + controller.abort() + await rejected + await collect() + assert.equal(liveCount([active.ref]), 1) + assert.equal(liveCount([queued.ref]), 0) + assert.equal(queue.retainedCallBytes, PAYLOAD_BYTES) + assert.equal(queue.queuedCallCount, 0) + hold.resolve() + assert.equal(await active.settled, 19) + await collect() + assert.equal(liveCount([active.ref, queued.ref]), 0) + assert.equal(queue.retainedCallBytes, 0) + assert.equal(queue.queues.size, 0) + return { + candidate, + case: 'active-and-cancelled', + lane, + activeRetainedUntilSettlement: true, + cancelledReleasedBeforeActiveFinishes: true + } +} + +async function checkFailure(Queue, candidate, lane) { + const queue = new Queue(3, 2) + const hold = liveCall(queue, 'terminal.send') + const method = lane === 'foreground' ? 'terminal.send' : 'git.status' + let failed = payloadCall(queue, method, Promise.resolve(), undefined, true) + const ref = failed.ref + await assert.rejects(failed.settled, { message: 'fixture failure 19' }) + failed = null + await collect() + const retained = liveCount([ref]) + assert.equal(retained, candidate ? 0 : 1) + assert.equal(queue.retainedCallBytes, 0) + hold.release() + await hold.settled + await collect() + assert.equal(liveCount([ref]), 0) + assert.equal(queue.queues.size, 0) + return { + candidate, + case: 'synchronous-failure', + lane, + retainedWhileOtherCallActive: retained, + retainedAfterIdle: 0 + } +} + +async function checkCrossLaneTraffic(Queue, candidate) { + const queue = new Queue(3, 2) + let current = liveCall(queue) + const refs = [] + for (let index = 0; index < 8; index++) { + const item = payloadCall(queue, 'terminal.send', Promise.resolve()) + refs.push(item.ref) + assert.equal(await item.settled, 19) + } + for (let index = 0; index < 70; index++) { + const next = liveCall(queue) + current.release() + await current.settled + current = next + } + await collect() + const retained = liveCount(refs) + assert.equal(retained, candidate ? 0 : 8) + assert.equal(queue.retainedCallBytes, 0) + assert.equal(queue.queues.get('fixture').active, 1) + assert.equal(queue.queues.get('fixture').foregroundHead, 8) + assert.equal(queue.queues.get('fixture').backgroundHead, 5) + current.release() + await current.settled + await collect() + assert.equal(liveCount(refs), 0) + assert.equal(queue.queues.size, 0) + return { + candidate, + case: 'rolling-background-traffic', + completedForegroundPayloads: 8, + completedBackgroundCalls: 70, + foregroundPayloadsStillRetained: retained, + retainedAfterIdle: 0, + everyBackgroundCallCompletes: true + } +} + +async function checkOrderAndCompaction(Queue, candidate) { + const queue = new Queue(1, 1) + const blocker = liveCall(queue, 'terminal.send') + const started = [] + const pending = [] + const controllers = [] + for (const lane of ['background', 'foreground']) { + for (let index = 0; index < 70; index++) { + const id = `${lane}:${index}` + const controller = new AbortController() + const promise = queue.enqueue( + 'fixture', + lane === 'background' ? 'git.status' : 'terminal.send', + async () => { + started.push(id) + return id + }, + 0, + controller.signal + ) + pending.push( + promise.then( + (value) => ({ value }), + (error) => ({ error: error.name }) + ) + ) + controllers.push({ id, controller }) + } + } + const cancelled = new Set([ + 'foreground:0', + 'foreground:35', + 'foreground:69', + 'background:0', + 'background:35', + 'background:69' + ]) + for (const { id, controller } of controllers) { + if (cancelled.has(id)) { + controller.abort() + } + } + assert.equal(queue.queuedCallCount, 134) + blocker.release() + await blocker.settled + const results = await Promise.all(pending) + const expected = ['foreground', 'background'] + .flatMap((lane) => Array.from({ length: 70 }, (_, index) => `${lane}:${index}`)) + .filter((id) => !cancelled.has(id)) + assert.deepEqual(started, expected) + assert.equal(results.filter((result) => result.error === 'AbortError').length, 6) + assert.equal(results.filter((result) => result.value !== undefined).length, 134) + await collect() + assert.equal(queue.queuedCallCount, 0) + assert.equal(queue.queues.size, 0) + return { + candidate, + case: 'ordering-compaction-and-cancellation', + completed: 134, + cancelled: 6, + foregroundBeforeBackground: true, + fifoWithinEachLane: true, + finalQueueCount: 0 + } +} + +async function main() { + const reports = [] + for (const candidate of [false, true]) { + const Queue = load(candidate) + for (const lane of ['foreground', 'background']) { + reports.push(await checkActiveAndCancelled(Queue, candidate, lane)) + reports.push(await checkFailure(Queue, candidate, lane)) + } + reports.push(await checkCrossLaneTraffic(Queue, candidate)) + reports.push(await checkOrderAndCompaction(Queue, candidate)) + } + const report = { + node: process.version, + electron: process.versions.electron ?? null, + versions, + proofSha256: sha(fs.readFileSync(__filename)), + reports + } + const resultName = process.versions.electron + ? 'electron-extended-results.json' + : 'extended-results.json' + fs.writeFileSync(path.join(__dirname, resultName), `${JSON.stringify(report, null, 2)}\n`) + console.log(JSON.stringify(report, null, 2)) +} +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) +setTimeout(() => { + console.error('fixture timeout') + process.exit(2) +}, 10000).unref() diff --git a/docs/audits/runtime-rpc-consumed-queue/extended-results.json b/docs/audits/runtime-rpc-consumed-queue/extended-results.json new file mode 100644 index 00000000000..c180564894e --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/extended-results.json @@ -0,0 +1,108 @@ +{ + "node": "v26.6.0", + "electron": null, + "versions": { + "sourcePath": "src/shared/runtime-rpc-call-queue.ts", + "baselineSha256": "45921658a10ffeefe0247673227aab643d1b5251d24aa460244fadd5480fc349", + "fixedSha256": "6afe6eb8acd9025d227960fafef77ae205a455c7f680b15ad0db5cdfee18c71d", + "matchingBaselineRefs": { + "origin/main": "77cd61df396f25ec91ee2d5ddcbd1f55aa94f818", + "v1.4.198": "e0826956fcfc532f5a1e55b5e081f2e57e553c43" + } + }, + "proofSha256": "af64f8b55f070d4a863ff6b8f456d518d977cc06400bdbbbba60eb2c5a455aa2", + "reports": [ + { + "candidate": false, + "case": "active-and-cancelled", + "lane": "foreground", + "activeRetainedUntilSettlement": true, + "cancelledReleasedBeforeActiveFinishes": true + }, + { + "candidate": false, + "case": "synchronous-failure", + "lane": "foreground", + "retainedWhileOtherCallActive": 1, + "retainedAfterIdle": 0 + }, + { + "candidate": false, + "case": "active-and-cancelled", + "lane": "background", + "activeRetainedUntilSettlement": true, + "cancelledReleasedBeforeActiveFinishes": true + }, + { + "candidate": false, + "case": "synchronous-failure", + "lane": "background", + "retainedWhileOtherCallActive": 1, + "retainedAfterIdle": 0 + }, + { + "candidate": false, + "case": "rolling-background-traffic", + "completedForegroundPayloads": 8, + "completedBackgroundCalls": 70, + "foregroundPayloadsStillRetained": 8, + "retainedAfterIdle": 0, + "everyBackgroundCallCompletes": true + }, + { + "candidate": false, + "case": "ordering-compaction-and-cancellation", + "completed": 134, + "cancelled": 6, + "foregroundBeforeBackground": true, + "fifoWithinEachLane": true, + "finalQueueCount": 0 + }, + { + "candidate": true, + "case": "active-and-cancelled", + "lane": "foreground", + "activeRetainedUntilSettlement": true, + "cancelledReleasedBeforeActiveFinishes": true + }, + { + "candidate": true, + "case": "synchronous-failure", + "lane": "foreground", + "retainedWhileOtherCallActive": 0, + "retainedAfterIdle": 0 + }, + { + "candidate": true, + "case": "active-and-cancelled", + "lane": "background", + "activeRetainedUntilSettlement": true, + "cancelledReleasedBeforeActiveFinishes": true + }, + { + "candidate": true, + "case": "synchronous-failure", + "lane": "background", + "retainedWhileOtherCallActive": 0, + "retainedAfterIdle": 0 + }, + { + "candidate": true, + "case": "rolling-background-traffic", + "completedForegroundPayloads": 8, + "completedBackgroundCalls": 70, + "foregroundPayloadsStillRetained": 0, + "retainedAfterIdle": 0, + "everyBackgroundCallCompletes": true + }, + { + "candidate": true, + "case": "ordering-compaction-and-cancellation", + "completed": 134, + "cancelled": 6, + "foregroundBeforeBackground": true, + "fifoWithinEachLane": true, + "finalQueueCount": 0 + } + ] +} diff --git a/docs/audits/runtime-rpc-consumed-queue/fix.patch b/docs/audits/runtime-rpc-consumed-queue/fix.patch new file mode 100644 index 00000000000..4a55e27feae --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/fix.patch @@ -0,0 +1,42 @@ +diff --git a/src/shared/runtime-rpc-call-queue.ts b/src/shared/runtime-rpc-call-queue.ts +index dcf4015078..7a3c184260 100644 +--- a/src/shared/runtime-rpc-call-queue.ts ++++ b/src/shared/runtime-rpc-call-queue.ts +@@ -30,9 +30,9 @@ type QueuedRuntimeCall = { + type RuntimeCallQueue = { + active: number + backgroundActive: number +- foreground: QueuedRuntimeCall[] ++ foreground: (QueuedRuntimeCall | undefined)[] + foregroundHead: number +- background: QueuedRuntimeCall[] ++ background: (QueuedRuntimeCall | undefined)[] + backgroundHead: number + } + +@@ -202,6 +202,7 @@ export class RuntimeRpcCallQueuePool { + return undefined + } + const call = queue.foreground[queue.foregroundHead] ++ queue.foreground[queue.foregroundHead] = undefined + queue.foregroundHead += 1 + this.queuedCallCount = Math.max(0, this.queuedCallCount - 1) + this.compactForeground(queue) +@@ -213,6 +214,7 @@ export class RuntimeRpcCallQueuePool { + return undefined + } + const call = queue.background[queue.backgroundHead] ++ queue.background[queue.backgroundHead] = undefined + queue.backgroundHead += 1 + this.queuedCallCount = Math.max(0, this.queuedCallCount - 1) + this.compactBackground(queue) +@@ -223,8 +225,7 @@ export class RuntimeRpcCallQueuePool { + if (queue.foregroundHead <= 32 || queue.foregroundHead * 2 < queue.foreground.length) { + return + } +- // Why: large remote-runtime refresh bursts can queue many calls; +- // head indexes avoid O(n) shift costs while compaction releases closures. ++ // Head indexes avoid repeated shifts; compaction bounds the consumed prefix. + queue.foreground.splice(0, queue.foregroundHead) + queue.foregroundHead = 0 + } diff --git a/docs/audits/runtime-rpc-consumed-queue/queue-source.cjs b/docs/audits/runtime-rpc-consumed-queue/queue-source.cjs new file mode 100644 index 00000000000..028b0a9b01c --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/queue-source.cjs @@ -0,0 +1,52 @@ +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 versions = require('./source-versions.json') + +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function') +const root = path.resolve(__dirname, '../../..') +const readText = (file) => readFileSync(file, 'utf8').replace(/\r\n/g, '\n') +const sha = (value) => createHash('sha256').update(value).digest('hex') +const fixedSource = readText(path.join(root, versions.sourcePath)) +assert.equal(sha(fixedSource), versions.fixedSha256, 'Product source changed; review proof hashes') +const patches = parsePatch(readText(path.join(__dirname, 'fix.patch'))) +assert.equal(patches.length, 1) +const baselineSource = applyPatch(fixedSource, reversePatch(patches[0])) +assert.notEqual(baselineSource, false) +assert.equal(sha(baselineSource), versions.baselineSha256, 'Baseline reconstruction changed') + +function load(candidate) { + const build = esbuild.buildSync({ + stdin: { + contents: candidate ? fixedSource : baselineSource, + resolveDir: path.join(root, 'src/shared'), + sourcefile: versions.sourcePath, + loader: 'ts' + }, + platform: 'node', + format: 'cjs', + bundle: true, + packages: 'external', + write: false + }) + const filename = path.join(__dirname, 'bundled-queue.cjs') + const loaded = new Module(filename, module) + loaded.filename = filename + loaded.paths = Module._nodeModulePaths(__dirname) + loaded._compile(build.outputFiles[0].text, filename) + return loaded.exports.RuntimeRpcCallQueuePool +} + +async function collect() { + for (let round = 0; round < 3; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + global.gc() + } +} + +module.exports = { load, collect, sha, versions, baselineSource } diff --git a/docs/audits/runtime-rpc-consumed-queue/reproduce.cjs b/docs/audits/runtime-rpc-consumed-queue/reproduce.cjs new file mode 100644 index 00000000000..228f779620e --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/reproduce.cjs @@ -0,0 +1,109 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { load, collect, sha, versions } = require('./queue-source.cjs') +async function postInput(queue, method, index) { + const data = new Uint8Array(1024 * 1024) + data[0] = index + const ref = new WeakRef(data) + await queue.enqueue('fixture', method, async () => data[0], data.byteLength) + return ref +} +async function postResult(queue, method, index) { + let ref + await queue.enqueue('fixture', method, async () => { + const data = new Uint8Array(1024 * 1024) + data[0] = index + ref = new WeakRef(data) + return { data } + }) + assert(ref) + return ref +} +async function lifetime(Queue, candidate, kind, lane, completed, releaseByIdle = false) { + const queue = new Queue(3, 2) + const hold = Promise.withResolvers() + const stuck = queue.enqueue('fixture', 'fixture.hold', () => hold.promise) + const method = lane === 'background' ? 'git.status' : 'fixture.echo' + const refs = [] + const before = process.memoryUsage() + for (let i = 0; i < completed; i++) { + refs.push(await (kind === 'input' ? postInput : postResult)(queue, method, i)) + } + await collect() + const retainedBeforeCompaction = refs.filter((ref) => ref.deref()).length + const { historyLength, head } = (() => { + const state = queue.queues.get('fixture') + assert(state) + return { historyLength: state[lane].length, head: state[`${lane}Head`] } + })() + assert.equal(queue.retainedCallBytes, 0) + assert.equal(queue.queuedCallCount, 0) + if (candidate || kind === 'input') { + assert.equal(retainedBeforeCompaction, candidate ? 0 : completed) + } + const afterCompleted = process.memoryUsage() + if (releaseByIdle) { + hold.resolve(0) + await stuck + } + const completionsUntilCompaction = releaseByIdle ? 0 : 33 - head + for (let i = 0; i < completionsUntilCompaction; i++) { + await queue.enqueue('fixture', method, async () => 0) + } + await collect() + const retainedAfterCompaction = refs.filter((ref) => ref.deref()).length + assert.equal(retainedAfterCompaction, 0) + hold.resolve(0) + await stuck + await collect() + assert.equal(queue.queues.size, 0) + return { + candidate, + kind, + lane, + completed, + releaseByIdle, + historyLength, + head, + retainedBeforeCompaction, + retainedAfterCompaction, + activeCreditBytesAfterCompleted: 0, + finalQueues: queue.queues.size, + memoryDelta: { + external: afterCompleted.external - before.external, + heapUsed: afterCompleted.heapUsed - before.heapUsed + } + } +} +async function main() { + const reports = [] + for (const candidate of [false, true]) { + const Queue = load(candidate) + for (const [kind, lane, completed, releaseByIdle] of [ + ['input', 'foreground', 8, false], + ['input', 'background', 8, false], + ['input', 'foreground', 8, true], + ['result', 'foreground', 8, false] + ]) { + reports.push(await lifetime(Queue, candidate, kind, lane, completed, releaseByIdle)) + console.log(JSON.stringify(reports.at(-1))) + } + } + const resultName = process.versions.electron ? 'electron-results.json' : 'results.json' + fs.writeFileSync( + path.join(__dirname, resultName), + `${JSON.stringify({ node: process.version, electron: process.versions.electron ?? null, versions, proofSha256: sha(fs.readFileSync(__filename)), reports }, null, 2)}\n` + ) +} +module.exports = { load, collect, sha } +if (require.main === module) { + main().catch((error) => { + console.error(error) + process.exitCode = 1 + }) + setTimeout(() => { + console.error('fixture timeout') + process.exit(2) + }, 10000).unref() +} diff --git a/docs/audits/runtime-rpc-consumed-queue/resolver-controls.cjs b/docs/audits/runtime-rpc-consumed-queue/resolver-controls.cjs new file mode 100644 index 00000000000..b10c4ab6bb1 --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/resolver-controls.cjs @@ -0,0 +1,55 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { collect, sha } = require('./queue-source.cjs') + +async function postResult(keepers) { + let ref + await new Promise((resolve) => { + const payload = new Uint8Array(1024 * 1024) + payload[0] = 19 + ref = new WeakRef(payload) + keepers.push(resolve) + resolve({ payload }) + }) + assert(ref) + return ref +} +async function main() { + const keepers = [] + const refs = [] + for (let index = 0; index < 8; index++) { + refs.push(await postResult(keepers)) + } + await collect() + const retainedWithResolveFunctions = refs.filter((ref) => ref.deref() !== undefined).length + keepers.length = 0 + await collect() + const retainedAfterResolveFunctionsReleased = refs.filter( + (ref) => ref.deref() !== undefined + ).length + assert.equal(retainedAfterResolveFunctionsReleased, 0) + const report = { + node: process.version, + electron: process.versions.electron ?? null, + v8: process.versions.v8, + proofSha256: sha(fs.readFileSync(__filename)), + payloads: 8, + bytesPerPayload: 1024 * 1024, + retainedWithResolveFunctions, + retainedAfterResolveFunctionsReleased + } + const resultName = process.versions.electron + ? 'electron-resolver-results.json' + : 'resolver-results.json' + fs.writeFileSync(path.join(__dirname, resultName), `${JSON.stringify(report, null, 2)}\n`) + console.log(JSON.stringify(report, null, 2)) +} +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) +setTimeout(() => { + console.error('fixture timeout') + process.exit(2) +}, 10000).unref() diff --git a/docs/audits/runtime-rpc-consumed-queue/resolver-results.json b/docs/audits/runtime-rpc-consumed-queue/resolver-results.json new file mode 100644 index 00000000000..f434de12456 --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/resolver-results.json @@ -0,0 +1,10 @@ +{ + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "proofSha256": "725087b61b9d79bc2daff4fe45f52951c531dcf9d23137a4939508957848350e", + "payloads": 8, + "bytesPerPayload": 1048576, + "retainedWithResolveFunctions": 0, + "retainedAfterResolveFunctionsReleased": 0 +} diff --git a/docs/audits/runtime-rpc-consumed-queue/results.json b/docs/audits/runtime-rpc-consumed-queue/results.json new file mode 100644 index 00000000000..245433ca6a3 --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/results.json @@ -0,0 +1,152 @@ +{ + "node": "v26.6.0", + "electron": null, + "versions": { + "sourcePath": "src/shared/runtime-rpc-call-queue.ts", + "baselineSha256": "45921658a10ffeefe0247673227aab643d1b5251d24aa460244fadd5480fc349", + "fixedSha256": "6afe6eb8acd9025d227960fafef77ae205a455c7f680b15ad0db5cdfee18c71d", + "matchingBaselineRefs": { + "origin/main": "77cd61df396f25ec91ee2d5ddcbd1f55aa94f818", + "v1.4.198": "e0826956fcfc532f5a1e55b5e081f2e57e553c43" + } + }, + "proofSha256": "79e415e5f7de6516b4e4bd8fc8ab0c6d0ac139efe996b8c39ef4cf7b8659bacb", + "reports": [ + { + "candidate": false, + "kind": "input", + "lane": "foreground", + "completed": 8, + "releaseByIdle": false, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 8, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 8378970, + "heapUsed": -555304 + } + }, + { + "candidate": false, + "kind": "input", + "lane": "background", + "completed": 8, + "releaseByIdle": false, + "historyLength": 8, + "head": 8, + "retainedBeforeCompaction": 8, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 8388608, + "heapUsed": 6648 + } + }, + { + "candidate": false, + "kind": "input", + "lane": "foreground", + "completed": 8, + "releaseByIdle": true, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 8, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 8388608, + "heapUsed": 192 + } + }, + { + "candidate": false, + "kind": "result", + "lane": "foreground", + "completed": 8, + "releaseByIdle": false, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 0, + "heapUsed": 7744 + } + }, + { + "candidate": true, + "kind": "input", + "lane": "foreground", + "completed": 8, + "releaseByIdle": false, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": -9748, + "heapUsed": -9552 + } + }, + { + "candidate": true, + "kind": "input", + "lane": "background", + "completed": 8, + "releaseByIdle": false, + "historyLength": 8, + "head": 8, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 0, + "heapUsed": -3808 + } + }, + { + "candidate": true, + "kind": "input", + "lane": "foreground", + "completed": 8, + "releaseByIdle": true, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 0, + "heapUsed": -4616 + } + }, + { + "candidate": true, + "kind": "result", + "lane": "foreground", + "completed": 8, + "releaseByIdle": false, + "historyLength": 9, + "head": 9, + "retainedBeforeCompaction": 0, + "retainedAfterCompaction": 0, + "activeCreditBytesAfterCompleted": 0, + "finalQueues": 0, + "memoryDelta": { + "external": 0, + "heapUsed": 2608 + } + } + ] +} diff --git a/docs/audits/runtime-rpc-consumed-queue/source-versions.json b/docs/audits/runtime-rpc-consumed-queue/source-versions.json new file mode 100644 index 00000000000..8fe55a755cf --- /dev/null +++ b/docs/audits/runtime-rpc-consumed-queue/source-versions.json @@ -0,0 +1,9 @@ +{ + "sourcePath": "src/shared/runtime-rpc-call-queue.ts", + "baselineSha256": "45921658a10ffeefe0247673227aab643d1b5251d24aa460244fadd5480fc349", + "fixedSha256": "6afe6eb8acd9025d227960fafef77ae205a455c7f680b15ad0db5cdfee18c71d", + "matchingBaselineRefs": { + "origin/main": "77cd61df396f25ec91ee2d5ddcbd1f55aa94f818", + "v1.4.198": "e0826956fcfc532f5a1e55b5e081f2e57e553c43" + } +} diff --git a/src/shared/runtime-rpc-call-queue-retention.test.ts b/src/shared/runtime-rpc-call-queue-retention.test.ts new file mode 100644 index 00000000000..838cb3b9e00 --- /dev/null +++ b/src/shared/runtime-rpc-call-queue-retention.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, it } from 'vitest' +import { RuntimeRpcCallQueuePool } from './runtime-rpc-call-queue' + +async function collect(): Promise { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + for (let round = 0; round < 3; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } +} + +function gate(): { promise: Promise; release: () => void } { + let release = (): void => {} + const promise = new Promise((resolve) => { + release = resolve + }) + return { promise, release } +} + +function enqueuePayload( + queue: RuntimeRpcCallQueuePool, + method: string, + wait: Promise, + signal?: AbortSignal +): { ref: WeakRef; settled: Promise } { + const payload = new Uint8Array(1024 * 1024) + payload[0] = 19 + return { + ref: new WeakRef(payload), + settled: queue.enqueue( + 'runtime-a', + method, + async () => { + await wait + return payload[0]! + }, + payload.byteLength, + signal + ) + } +} + +async function completePayload( + queue: RuntimeRpcCallQueuePool, + method: string +): Promise> { + const { ref, settled } = enqueuePayload(queue, method, Promise.resolve()) + expect(await settled).toBe(19) + return ref +} + +describe('runtime RPC completed-call retention', () => { + it.each(['terminal.send', 'git.status'])( + 'releases completed %s inputs while another call keeps the selector active', + async (method) => { + const queue = new RuntimeRpcCallQueuePool(3, 2) + const blocker = gate() + const active = queue.enqueue('runtime-a', 'terminal.send', () => blocker.promise) + try { + const refs: WeakRef[] = [] + for (let index = 0; index < 8; index += 1) { + refs.push(await completePayload(queue, method)) + } + await collect() + expect(refs.filter((ref) => ref.deref() !== undefined)).toHaveLength(0) + } finally { + blocker.release() + await active + } + } + ) + + it.each(['terminal.send', 'git.status'])( + 'keeps an active %s input and releases a cancelled queued input', + async (method) => { + const queue = new RuntimeRpcCallQueuePool(1, 1) + const blocker = gate() + const active = enqueuePayload(queue, method, blocker.promise) + const controller = new AbortController() + const queued = enqueuePayload(queue, method, Promise.resolve(), controller.signal) + const rejected = expect(queued.settled).rejects.toMatchObject({ name: 'AbortError' }) + try { + await collect() + expect(active.ref.deref()?.[0]).toBe(19) + expect(queued.ref.deref()?.[0]).toBe(19) + controller.abort() + await rejected + await collect() + expect(active.ref.deref()?.[0]).toBe(19) + expect(queued.ref.deref() === undefined).toBe(true) + } finally { + controller.abort() + blocker.release() + await Promise.allSettled([active.settled, queued.settled]) + } + expect(await active.settled).toBe(19) + await collect() + expect(active.ref.deref() === undefined).toBe(true) + } + ) + + it('releases old foreground inputs during continuous finite background calls', async () => { + const queue = new RuntimeRpcCallQueuePool(3, 2) + const startBackground = (): { release: () => void; settled: Promise } => { + const wait = gate() + return { + release: wait.release, + settled: queue.enqueue('runtime-a', 'git.status', () => wait.promise) + } + } + let active = startBackground() + try { + const ref = await completePayload(queue, 'terminal.send') + for (let index = 0; index < 70; index += 1) { + const next = startBackground() + active.release() + await active.settled + active = next + } + await collect() + expect(ref.deref() === undefined).toBe(true) + } finally { + active.release() + await active.settled + } + }) + + it('preserves lane order and queued cancellation across compaction', async () => { + const queue = new RuntimeRpcCallQueuePool(1, 1) + const blocker = gate() + const active = queue.enqueue('runtime-a', 'terminal.send', () => blocker.promise) + const started: string[] = [] + const pending: Promise[] = [] + const cancelled = new Set([ + 'foreground:0', + 'foreground:35', + 'foreground:69', + 'background:0', + 'background:35', + 'background:69' + ]) + for (const lane of ['background', 'foreground']) { + for (let index = 0; index < 70; index += 1) { + const id = `${lane}:${index}` + const controller = new AbortController() + const settled = queue.enqueue( + 'runtime-a', + lane === 'background' ? 'git.status' : 'terminal.send', + async () => { + started.push(id) + return id + }, + 0, + controller.signal + ) + if (cancelled.has(id)) { + pending.push( + settled.catch((error: unknown) => { + expect(error).toMatchObject({ name: 'AbortError' }) + return 'cancelled' + }) + ) + controller.abort() + } else { + pending.push(settled) + } + } + } + blocker.release() + await active + const results = await Promise.all(pending) + const expected = ['foreground', 'background'] + .flatMap((lane) => Array.from({ length: 70 }, (_, index) => `${lane}:${index}`)) + .filter((id) => !cancelled.has(id)) + expect(started).toEqual(expected) + expect(results.filter((value) => value === 'cancelled')).toHaveLength(6) + expect(await queue.enqueue('runtime-a', 'terminal.send', async () => 'recovered')).toBe( + 'recovered' + ) + }) +}) diff --git a/src/shared/runtime-rpc-call-queue.ts b/src/shared/runtime-rpc-call-queue.ts index dcf40150789..7a3c1842602 100644 --- a/src/shared/runtime-rpc-call-queue.ts +++ b/src/shared/runtime-rpc-call-queue.ts @@ -30,9 +30,9 @@ type QueuedRuntimeCall = { type RuntimeCallQueue = { active: number backgroundActive: number - foreground: QueuedRuntimeCall[] + foreground: (QueuedRuntimeCall | undefined)[] foregroundHead: number - background: QueuedRuntimeCall[] + background: (QueuedRuntimeCall | undefined)[] backgroundHead: number } @@ -202,6 +202,7 @@ export class RuntimeRpcCallQueuePool { return undefined } const call = queue.foreground[queue.foregroundHead] + queue.foreground[queue.foregroundHead] = undefined queue.foregroundHead += 1 this.queuedCallCount = Math.max(0, this.queuedCallCount - 1) this.compactForeground(queue) @@ -213,6 +214,7 @@ export class RuntimeRpcCallQueuePool { return undefined } const call = queue.background[queue.backgroundHead] + queue.background[queue.backgroundHead] = undefined queue.backgroundHead += 1 this.queuedCallCount = Math.max(0, this.queuedCallCount - 1) this.compactBackground(queue) @@ -223,8 +225,7 @@ export class RuntimeRpcCallQueuePool { if (queue.foregroundHead <= 32 || queue.foregroundHead * 2 < queue.foreground.length) { return } - // Why: large remote-runtime refresh bursts can queue many calls; - // head indexes avoid O(n) shift costs while compaction releases closures. + // Head indexes avoid repeated shifts; compaction bounds the consumed prefix. queue.foreground.splice(0, queue.foregroundHead) queue.foregroundHead = 0 } From f90370fb6b072847f625b4e1291a4927fe3c5ec4 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:05 -0700 Subject: [PATCH 11/59] fix: detach aborted shared auth filesystem waits (#21135) Co-authored-by: m4air --- .../auth-filesystem-wait-retention/README.md | 32 + .../before.config.mjs | 24 + .../electron-results.json | 598 ++++++++++++++++++ .../auth-filesystem-wait-retention/fix.patch | 98 +++ .../node-results.json | 597 +++++++++++++++++ .../reproduce.cjs | 242 +++++++ .../settlement-order.cjs | 37 ++ .../source-versions.json | 11 + .../sources.cjs | 29 + .../validation.json | 61 ++ ...uth-filesystem-operation-retention.test.ts | 166 +++++ .../rate-limits/auth-filesystem-operation.ts | 32 +- src/shared/promise-settlement-waiters.test.ts | 37 ++ src/shared/promise-settlement-waiters.ts | 16 +- 14 files changed, 1959 insertions(+), 21 deletions(-) create mode 100644 docs/audits/auth-filesystem-wait-retention/README.md create mode 100644 docs/audits/auth-filesystem-wait-retention/before.config.mjs create mode 100644 docs/audits/auth-filesystem-wait-retention/electron-results.json create mode 100644 docs/audits/auth-filesystem-wait-retention/fix.patch create mode 100644 docs/audits/auth-filesystem-wait-retention/node-results.json create mode 100644 docs/audits/auth-filesystem-wait-retention/reproduce.cjs create mode 100644 docs/audits/auth-filesystem-wait-retention/settlement-order.cjs create mode 100644 docs/audits/auth-filesystem-wait-retention/source-versions.json create mode 100644 docs/audits/auth-filesystem-wait-retention/sources.cjs create mode 100644 docs/audits/auth-filesystem-wait-retention/validation.json create mode 100644 src/main/rate-limits/auth-filesystem-operation-retention.test.ts diff --git a/docs/audits/auth-filesystem-wait-retention/README.md b/docs/audits/auth-filesystem-wait-retention/README.md new file mode 100644 index 00000000000..fb97929d597 --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/README.md @@ -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. diff --git a/docs/audits/auth-filesystem-wait-retention/before.config.mjs b/docs/audits/auth-filesystem-wait-retention/before.config.mjs new file mode 100644 index 00000000000..3f9a6aa4360 --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/before.config.mjs @@ -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 } + } + } + ] + }) +) diff --git a/docs/audits/auth-filesystem-wait-retention/electron-results.json b/docs/audits/auth-filesystem-wait-retention/electron-results.json new file mode 100644 index 00000000000..1fa3f9e51d7 --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/electron-results.json @@ -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" + } +} diff --git a/docs/audits/auth-filesystem-wait-retention/fix.patch b/docs/audits/auth-filesystem-wait-retention/fix.patch new file mode 100644 index 00000000000..dce5d0187de --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/fix.patch @@ -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() +@@ -139,10 +140,9 @@ export function createAuthFilesystemOperation( + const waiters = new Set() + 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( + + const waiter = Symbol('auth-filesystem-waiter') + waiters.add(waiter) +- let onAbort: (() => void) | null = null +- const aborted = new Promise((_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 = { + + export type PromiseSettlementWaitOptions = { + 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 { + } + return new Promise((resolve, reject) => { + let waiter!: PromiseSettlementWaiter +- 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 { + 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, diff --git a/docs/audits/auth-filesystem-wait-retention/node-results.json b/docs/audits/auth-filesystem-wait-retention/node-results.json new file mode 100644 index 00000000000..fd9974f64fb --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/node-results.json @@ -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" + } +} diff --git a/docs/audits/auth-filesystem-wait-retention/reproduce.cjs b/docs/audits/auth-filesystem-wait-retention/reproduce.cjs new file mode 100644 index 00000000000..acc537f286f --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/reproduce.cjs @@ -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)) diff --git a/docs/audits/auth-filesystem-wait-retention/settlement-order.cjs b/docs/audits/auth-filesystem-wait-retention/settlement-order.cjs new file mode 100644 index 00000000000..2a830f7042a --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/settlement-order.cjs @@ -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 +} diff --git a/docs/audits/auth-filesystem-wait-retention/source-versions.json b/docs/audits/auth-filesystem-wait-retention/source-versions.json new file mode 100644 index 00000000000..c548dfe05f5 --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/source-versions.json @@ -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 +} diff --git a/docs/audits/auth-filesystem-wait-retention/sources.cjs b/docs/audits/auth-filesystem-wait-retention/sources.cjs new file mode 100644 index 00000000000..94a8c1da504 --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/sources.cjs @@ -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 } +} diff --git a/docs/audits/auth-filesystem-wait-retention/validation.json b/docs/audits/auth-filesystem-wait-retention/validation.json new file mode 100644 index 00000000000..d9732979b1e --- /dev/null +++ b/docs/audits/auth-filesystem-wait-retention/validation.json @@ -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" + } +} diff --git a/src/main/rate-limits/auth-filesystem-operation-retention.test.ts b/src/main/rate-limits/auth-filesystem-operation-retention.test.ts new file mode 100644 index 00000000000..a79cfd63af9 --- /dev/null +++ b/src/main/rate-limits/auth-filesystem-operation-retention.test.ts @@ -0,0 +1,166 @@ +import { getEventListeners } from 'node:events' +import { describe, expect, it, vi } from 'vitest' +import { + createAuthFilesystemOperation, + type SharedAuthFilesystemOperation +} from './auth-filesystem-operation' + +function pendingOperation(): { + operation: SharedAuthFilesystemOperation + resolve: (value: string) => void + reject: (reason: unknown) => void + rawCalls: () => number +} { + let resolve = (_value: string): void => {} + let reject = (_reason: unknown): void => {} + let calls = 0 + const operation = createAuthFilesystemOperation('auth-retention-fixture', () => { + calls += 1 + return new Promise((resolveRaw, rejectRaw) => { + resolve = resolveRaw + reject = rejectRaw + }) + }) + return { + operation, + resolve: (value) => resolve(value), + reject: (reason) => reject(reason), + rawCalls: () => calls + } +} + +async function abortWait( + operation: SharedAuthFilesystemOperation +): Promise> { + const controller = new AbortController() + const reason = new Error('Auth poll expired') + const weakReason = new WeakRef(reason) + const result = operation.wait(controller.signal) + controller.abort(reason) + await result.catch((error: unknown) => { + if (error !== reason) { + throw new Error('Abort reason identity changed') + } + }) + expect(getEventListeners(controller.signal, 'abort')).toHaveLength(0) + return weakReason +} + +async function collect(): Promise { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + for (let round = 0; round < 5; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } +} + +describe('shared auth filesystem wait lifetime', () => { + it.each( + [true, false].flatMap((startedBefore) => + [true, false].flatMap((rejectRaw) => + [0, 1].map((ticks) => ({ startedBefore, rejectRaw, ticks })) + ) + ) + )('preserves raw-result/abort ordering for %j', async ({ startedBefore, rejectRaw, ticks }) => { + const pending = pendingOperation() + const controller = new AbortController() + await new Promise((resolve) => setImmediate(resolve)) + const start = (): Promise => + pending.operation.wait(controller.signal).then( + (value) => ({ status: 'fulfilled', value }), + (reason: unknown) => ({ status: 'rejected', reason }) + ) + let waiting = startedBefore ? start() : undefined + if (rejectRaw) { + pending.reject('raw failure') + } else { + pending.resolve('raw success') + } + for (let tick = 0; tick < ticks; tick += 1) { + await Promise.resolve() + } + waiting ??= start() + controller.abort('caller aborted') + expect(await waiting).toEqual( + ticks === 0 + ? { status: 'rejected', reason: 'caller aborted' } + : rejectRaw + ? { status: 'rejected', reason: 'raw failure' } + : { status: 'fulfilled', value: 'raw success' } + ) + expect(pending.rawCalls()).toBe(1) + expect(getEventListeners(controller.signal, 'abort')).toHaveLength(0) + }) + + it('releases aborted poll reasons while one native operation remains needed', async () => { + const pending = pendingOperation() + const anchorController = new AbortController() + const anchor = pending.operation.wait(anchorController.signal) + await Promise.resolve() + const thenSpy = vi.spyOn(pending.operation.result, 'then') + try { + const reasons: WeakRef[] = [] + for (let index = 0; index < 64; index += 1) { + reasons.push(await abortWait(pending.operation)) + } + await collect() + expect(reasons.filter((ref) => ref.deref() !== undefined)).toHaveLength(0) + expect(pending.rawCalls()).toBe(1) + // Each native-result reaction would outlive every abandoned poll. + expect(thenSpy).not.toHaveBeenCalled() + } finally { + thenSpy.mockRestore() + pending.resolve('finished') + await anchor + } + expect(getEventListeners(anchorController.signal, 'abort')).toHaveLength(0) + }) + + it('serves a late and then settled result after all previous polls abort', async () => { + const pending = pendingOperation() + await Promise.resolve() + await abortWait(pending.operation) + const controller = new AbortController() + const late = pending.operation.wait(controller.signal) + pending.resolve('late result') + await expect(late).resolves.toBe('late result') + await expect(pending.operation.wait(controller.signal)).resolves.toBe('late result') + expect(pending.rawCalls()).toBe(1) + expect(getEventListeners(controller.signal, 'abort')).toHaveLength(0) + }) + + it('preserves a live sibling and forwards raw failure identity to later waits', async () => { + const pending = pendingOperation() + const controller = new AbortController() + const live = pending.operation.wait(controller.signal) + await Promise.resolve() + await abortWait(pending.operation) + const reason = new Error('Raw filesystem failure') + const rejected = expect(live).rejects.toBe(reason) + pending.reject(reason) + await rejected + await expect(pending.operation.wait(controller.signal)).rejects.toBe(reason) + expect(pending.rawCalls()).toBe(1) + expect(getEventListeners(controller.signal, 'abort')).toHaveLength(0) + }) + + it.each([false, 0, 'custom abort', { code: 'custom abort' }])( + 'preserves the arbitrary abort reason %j', + async (reason) => { + const pending = pendingOperation() + const controller = new AbortController() + await Promise.resolve() + const wait = pending.operation.wait(controller.signal) + controller.abort(reason) + try { + await expect(wait).rejects.toBe(reason) + expect(getEventListeners(controller.signal, 'abort')).toHaveLength(0) + } finally { + pending.resolve('finished') + await pending.operation.result + } + } + ) +}) diff --git a/src/main/rate-limits/auth-filesystem-operation.ts b/src/main/rate-limits/auth-filesystem-operation.ts index 228234e92c0..7d030ab01e3 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() @@ -139,10 +140,9 @@ export function createAuthFilesystemOperation( const waiters = new Set() 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( const waiter = Symbol('auth-filesystem-waiter') waiters.add(waiter) - let onAbort: (() => void) | null = null - const aborted = new Promise((_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.test.ts b/src/shared/promise-settlement-waiters.test.ts index dfb9b50b7bb..0af1c1c61ef 100644 --- a/src/shared/promise-settlement-waiters.test.ts +++ b/src/shared/promise-settlement-waiters.test.ts @@ -2,6 +2,43 @@ import { describe, expect, it, vi } from 'vitest' import { PromiseSettlementWaiters } from './promise-settlement-waiters' describe('PromiseSettlementWaiters', () => { + it.each([false, true])('preserves abort scheduling with abortInMicrotask=%s', async (defer) => { + let resolveBase = (_value: string): void => {} + const base = new Promise((resolve) => { + resolveBase = resolve + }) + const waiters = new PromiseSettlementWaiters(base) + const controller = new AbortController() + const reason = { code: 'aborted' } + const wait = waiters.wait({ + signal: controller.signal, + abortInMicrotask: defer, + createAbortError: () => reason + }) + resolveBase('raw result') + controller.abort() + await (defer ? expect(wait).resolves.toBe('raw result') : expect(wait).rejects.toBe(reason)) + expect(waiters.waiterCount).toBe(0) + }) + + it('lets an earlier deferred abort win over a later raw settlement', async () => { + let resolveBase = (_value: string): void => {} + const base = new Promise((resolve) => { + resolveBase = resolve + }) + const waiters = new PromiseSettlementWaiters(base) + const controller = new AbortController() + const wait = waiters.wait({ + signal: controller.signal, + abortInMicrotask: true, + createAbortError: () => false + }) + controller.abort() + resolveBase('raw result') + await expect(wait).rejects.toBe(false) + expect(waiters.waiterCount).toBe(0) + }) + it('removes ten thousand aborted callers while one anchor remains pending', async () => { let resolveBase: (value: number) => void = () => {} const basePromise = new Promise((resolve) => { diff --git a/src/shared/promise-settlement-waiters.ts b/src/shared/promise-settlement-waiters.ts index 98ec24b25c1..97230d54dd6 100644 --- a/src/shared/promise-settlement-waiters.ts +++ b/src/shared/promise-settlement-waiters.ts @@ -13,8 +13,10 @@ type PromiseSettlementWaiter = { export type PromiseSettlementWaitOptions = { 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 { } return new Promise((resolve, reject) => { let waiter!: PromiseSettlementWaiter - 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 { 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, From 51f809aa82b58343a38cdba1920190bfd01da3a0 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:08 -0700 Subject: [PATCH 12/59] fix: retire obsolete GitLab host cache generations (#21136) Co-authored-by: m4air --- .../gitlab-known-host-retirement/README.md | 39 +++ .../electron-results.json | 89 ++++++ .../gitlab-known-host-retirement/fix.patch | 103 +++++++ .../original-source-hashes.json | 3 + .../reproduce.cjs | 257 ++++++++++++++++++ .../gitlab-known-host-retirement/results.json | 88 ++++++ .../gitlab-known-host-retirement/sources.cjs | 30 ++ src/main/gitlab/gitlab-known-host-probe.ts | 44 ++- .../gitlab-known-host-retirement.test.ts | 171 ++++++++++++ 9 files changed, 813 insertions(+), 11 deletions(-) create mode 100644 docs/audits/gitlab-known-host-retirement/README.md create mode 100644 docs/audits/gitlab-known-host-retirement/electron-results.json create mode 100644 docs/audits/gitlab-known-host-retirement/fix.patch create mode 100644 docs/audits/gitlab-known-host-retirement/original-source-hashes.json create mode 100644 docs/audits/gitlab-known-host-retirement/reproduce.cjs create mode 100644 docs/audits/gitlab-known-host-retirement/results.json create mode 100644 docs/audits/gitlab-known-host-retirement/sources.cjs create mode 100644 src/main/gitlab/gitlab-known-host-retirement.test.ts diff --git a/docs/audits/gitlab-known-host-retirement/README.md b/docs/audits/gitlab-known-host-retirement/README.md new file mode 100644 index 00000000000..249a7ad8fbc --- /dev/null +++ b/docs/audits/gitlab-known-host-retirement/README.md @@ -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. diff --git a/docs/audits/gitlab-known-host-retirement/electron-results.json b/docs/audits/gitlab-known-host-retirement/electron-results.json new file mode 100644 index 00000000000..8dda9c71946 --- /dev/null +++ b/docs/audits/gitlab-known-host-retirement/electron-results.json @@ -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" + } + } +} diff --git a/docs/audits/gitlab-known-host-retirement/fix.patch b/docs/audits/gitlab-known-host-retirement/fix.patch new file mode 100644 index 00000000000..61fb557d1d3 --- /dev/null +++ b/docs/audits/gitlab-known-host-retirement/fix.patch @@ -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() ++const knownHostsCacheByExecutionContext = new Map< ++ string, ++ { key: string; hosts: readonly string[] } ++>() + const knownHostsInFlightByExecutionContext: CoalescedProbes = new Map() + const unauthenticatedHostExpiries = new Map() + +@@ -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 { +- 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 { +@@ -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] + } + } + diff --git a/docs/audits/gitlab-known-host-retirement/original-source-hashes.json b/docs/audits/gitlab-known-host-retirement/original-source-hashes.json new file mode 100644 index 00000000000..c7b307638fe --- /dev/null +++ b/docs/audits/gitlab-known-host-retirement/original-source-hashes.json @@ -0,0 +1,3 @@ +{ + "src/main/gitlab/gitlab-known-host-probe.ts": "4f9651c5a383438aa968aec082c200a3371e2f4317372125aa11aa6938793c25" +} diff --git a/docs/audits/gitlab-known-host-retirement/reproduce.cjs b/docs/audits/gitlab-known-host-retirement/reproduce.cjs new file mode 100644 index 00000000000..176613137a8 --- /dev/null +++ b/docs/audits/gitlab-known-host-retirement/reproduce.cjs @@ -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() diff --git a/docs/audits/gitlab-known-host-retirement/results.json b/docs/audits/gitlab-known-host-retirement/results.json new file mode 100644 index 00000000000..41495555ade --- /dev/null +++ b/docs/audits/gitlab-known-host-retirement/results.json @@ -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" + } + } +} diff --git a/docs/audits/gitlab-known-host-retirement/sources.cjs b/docs/audits/gitlab-known-host-retirement/sources.cjs new file mode 100644 index 00000000000..1effdb26ecf --- /dev/null +++ b/docs/audits/gitlab-known-host-retirement/sources.cjs @@ -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 } diff --git a/src/main/gitlab/gitlab-known-host-probe.ts b/src/main/gitlab/gitlab-known-host-probe.ts index 752e1b0291e..d328cd38cac 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() +const knownHostsCacheByExecutionContext = new Map< + string, + { key: string; hosts: readonly string[] } +>() const knownHostsInFlightByExecutionContext: CoalescedProbes = new Map() const unauthenticatedHostExpiries = new Map() @@ -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 { - 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 { @@ -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] } } diff --git a/src/main/gitlab/gitlab-known-host-retirement.test.ts b/src/main/gitlab/gitlab-known-host-retirement.test.ts new file mode 100644 index 00000000000..60bb2be13b2 --- /dev/null +++ b/src/main/gitlab/gitlab-known-host-retirement.test.ts @@ -0,0 +1,171 @@ +import { afterEach, beforeEach, expect, it, vi } from 'vitest' + +const { execute, generations } = vi.hoisted(() => ({ + execute: vi.fn(), + generations: new Map() +})) +vi.mock('../git/runner', () => ({ glabExecFileAsync: execute })) +vi.mock('../providers/ssh-git-dispatch', () => ({ + getSshGitProviderGeneration: (connectionId: string) => generations.get(connectionId) ?? 0 +})) + +import { + _resetKnownHostsCache, + getGlabKnownHosts, + rememberGlabKnownHost +} from './gitlab-known-host-probe' +import { PROBE_COALESCE_STALE_MS } from '../git/coalesced-probe' + +const response = (host: string) => ({ stdout: `Logged in to ${host} as user`, stderr: '' }) +const deferred = () => Promise.withResolvers>() + +async function collect(): Promise { + if (typeof globalThis.gc !== 'function') { + throw new Error('Run with the repository Vitest --expose-gc config') + } + for (let index = 0; index < 6; index++) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } + await new Promise((resolve) => setImmediate(resolve)) +} + +async function weakResult(connectionId: string): Promise> { + return new WeakRef(await getGlabKnownHosts(connectionId)) +} + +beforeEach(() => { + _resetKnownHostsCache() + generations.clear() + execute.mockReset() +}) +afterEach(() => { + _resetKnownHostsCache() + vi.restoreAllMocks() +}) + +it('releases successful host arrays from superseded SSH generations', async () => { + const results: WeakRef[] = [] + for (let generation = 1; generation <= 32; generation++) { + generations.set('connection', generation) + execute.mockResolvedValue(response(`host${generation}.test`)) + results.push(await weakResult('connection')) + } + await collect() + expect(results.filter((result) => result.deref() !== undefined)).toHaveLength(1) + await expect(getGlabKnownHosts('connection')).resolves.toEqual(['gitlab.com', 'host32.test']) + expect(execute).toHaveBeenCalledTimes(32) +}) + +it('retires remembered generations without requiring an auth-status probe', async () => { + const results: WeakRef[] = [] + for (let generation = 1; generation <= 16; generation++) { + generations.set('connection', generation) + rememberGlabKnownHost(`host${generation}.test`, 'connection') + results.push(await weakResult('connection')) + } + await collect() + expect(results.filter((result) => result.deref() !== undefined)).toHaveLength(1) + await expect(getGlabKnownHosts('connection')).resolves.toEqual(['gitlab.com', 'host16.test']) + expect(execute).not.toHaveBeenCalled() +}) + +it('does not retain a delayed old-generation result after a replacement answers', async () => { + const old = deferred() + generations.set('connection', 1) + execute.mockReturnValueOnce(old.promise) + const oldResult = weakResult('connection') + generations.set('connection', 2) + execute.mockResolvedValueOnce(response('replacement.test')) + await expect(getGlabKnownHosts('connection')).resolves.toEqual(['gitlab.com', 'replacement.test']) + old.resolve(response('retired.test')) + const reference = await oldResult + await collect() + expect(reference.deref()).toBeUndefined() + await expect(getGlabKnownHosts('connection')).resolves.toEqual(['gitlab.com', 'replacement.test']) + expect(execute).toHaveBeenCalledTimes(2) +}) + +it('does not repopulate an explicitly reset cache from an earlier probe', async () => { + const old = deferred() + execute.mockReturnValueOnce(old.promise) + const oldResult = getGlabKnownHosts() + _resetKnownHostsCache() + old.resolve(response('before-reset.test')) + await expect(oldResult).resolves.toEqual(['gitlab.com', 'before-reset.test']) + execute.mockResolvedValueOnce(response('after-reset.test')) + await expect(getGlabKnownHosts()).resolves.toEqual(['gitlab.com', 'after-reset.test']) + expect(execute).toHaveBeenCalledTimes(2) +}) + +it('keeps a post-reset successor joinable when the old probe settles first', async () => { + const old = deferred() + const next = deferred() + execute.mockReturnValueOnce(old.promise).mockReturnValueOnce(next.promise) + const oldResult = getGlabKnownHosts() + _resetKnownHostsCache() + const nextResult = getGlabKnownHosts() + old.resolve(response('before-reset.test')) + await oldResult + let joinedSettled = false + const joined = getGlabKnownHosts().then((hosts) => { + joinedSettled = true + return hosts + }) + await Promise.resolve() + expect(joinedSettled).toBe(false) + next.resolve(response('after-reset.test')) + await expect(nextResult).resolves.toEqual(['gitlab.com', 'after-reset.test']) + await expect(joined).resolves.toEqual(['gitlab.com', 'after-reset.test']) + expect(execute).toHaveBeenCalledTimes(2) +}) + +it('prevents an abandoned same-generation probe from publishing over its successor', async () => { + const clock = vi.spyOn(Date, 'now').mockReturnValue(1000) + const old = deferred() + execute.mockReturnValueOnce(old.promise) + const oldResult = getGlabKnownHosts('connection') + clock.mockReturnValue(1000 + PROBE_COALESCE_STALE_MS + 1) + execute.mockResolvedValueOnce(response('replacement.test')) + await expect(getGlabKnownHosts('connection')).resolves.toEqual(['gitlab.com', 'replacement.test']) + old.resolve(response('abandoned.test')) + await oldResult + await expect(getGlabKnownHosts('connection')).resolves.toEqual(['gitlab.com', 'replacement.test']) + expect(execute).toHaveBeenCalledTimes(2) +}) + +it('keeps native, WSL and other connection caches when one generation changes', async () => { + execute + .mockResolvedValueOnce(response('native.test')) + .mockResolvedValueOnce(response('ubuntu.test')) + .mockResolvedValueOnce(response('debian.test')) + .mockResolvedValueOnce(response('connection-a.test')) + .mockResolvedValueOnce(response('connection-b.test')) + const native = await getGlabKnownHosts() + const ubuntu = await getGlabKnownHosts(undefined, { wslDistro: 'Ubuntu' }) + const debian = await getGlabKnownHosts(undefined, { wslDistro: 'Debian' }) + await getGlabKnownHosts('connection-a') + const other = await getGlabKnownHosts('connection-b') + generations.set('connection-a', 1) + rememberGlabKnownHost('replacement.test', 'connection-a') + await expect(getGlabKnownHosts('connection-a')).resolves.toEqual([ + 'gitlab.com', + 'replacement.test' + ]) + await expect(getGlabKnownHosts()).resolves.toBe(native) + await expect(getGlabKnownHosts(undefined, { wslDistro: 'Ubuntu' })).resolves.toBe(ubuntu) + await expect(getGlabKnownHosts(undefined, { wslDistro: 'Debian' })).resolves.toBe(debian) + await expect(getGlabKnownHosts('connection-b')).resolves.toBe(other) + expect(execute).toHaveBeenCalledTimes(5) +}) + +it('does not serve a retired generation after the current probe fails', async () => { + execute.mockResolvedValueOnce(response('retired.test')) + await getGlabKnownHosts('connection') + generations.set('connection', 1) + execute.mockRejectedValueOnce(new Error('current host unavailable')) + await expect(getGlabKnownHosts('connection')).resolves.toEqual(['gitlab.com']) + execute.mockResolvedValueOnce(response('current.test')) + await expect(getGlabKnownHosts('connection')).resolves.toEqual(['gitlab.com', 'current.test']) + expect(execute).toHaveBeenCalledTimes(3) +}) From fbfe3a2e74043673ccc928554f6fd667493cbb4a Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:11 -0700 Subject: [PATCH 13/59] fix: release Codex prompt claims when their turns complete (#21138) Co-authored-by: m4air --- .../codex-prompt-claim-retention/README.md | 53 + .../before.config.mjs | 24 + .../electron-results.json | 1239 +++++++++++++++++ .../codex-prompt-claim-retention/fix.patch | 11 + .../node-results.json | 1238 ++++++++++++++++ .../reproduce.cjs | 111 ++ .../codex-prompt-claim-retention/scenario.cjs | 215 +++ .../source-versions.json | 54 + .../codex-prompt-claim-retention/sources.cjs | 29 + .../validation.json | 69 + .../codex-prompt-registry-retention.test.ts | 110 ++ src/main/codex/codex-prompt-registry.ts | 2 +- 12 files changed, 3154 insertions(+), 1 deletion(-) create mode 100644 docs/audits/codex-prompt-claim-retention/README.md create mode 100644 docs/audits/codex-prompt-claim-retention/before.config.mjs create mode 100644 docs/audits/codex-prompt-claim-retention/electron-results.json create mode 100644 docs/audits/codex-prompt-claim-retention/fix.patch create mode 100644 docs/audits/codex-prompt-claim-retention/node-results.json create mode 100644 docs/audits/codex-prompt-claim-retention/reproduce.cjs create mode 100644 docs/audits/codex-prompt-claim-retention/scenario.cjs create mode 100644 docs/audits/codex-prompt-claim-retention/source-versions.json create mode 100644 docs/audits/codex-prompt-claim-retention/sources.cjs create mode 100644 docs/audits/codex-prompt-claim-retention/validation.json create mode 100644 src/main/codex/codex-prompt-registry-retention.test.ts diff --git a/docs/audits/codex-prompt-claim-retention/README.md b/docs/audits/codex-prompt-claim-retention/README.md new file mode 100644 index 00000000000..5a617232d0b --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/README.md @@ -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. diff --git a/docs/audits/codex-prompt-claim-retention/before.config.mjs b/docs/audits/codex-prompt-claim-retention/before.config.mjs new file mode 100644 index 00000000000..1a6483dfb9d --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/before.config.mjs @@ -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 } + } + } + ] + }) +) diff --git a/docs/audits/codex-prompt-claim-retention/electron-results.json b/docs/audits/codex-prompt-claim-retention/electron-results.json new file mode 100644 index 00000000000..56116e4e20d --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/electron-results.json @@ -0,0 +1,1239 @@ +{ + "capturedAt": "2026-09-17T02:39:39.480Z", + "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" + }, + "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": { + "src/main/codex/codex-prompt-registry.ts": { + "before": "254c530216f47abfc3572bb1689161ad3c10775af2cdceda7e94f443e52c0691", + "after": "3d33f98215b9547d661c0a4f6aa8b9a8a6a88fa953d90652cb3fb9953425a539" + } + }, + "countsOnly": true, + "noPayloadAmplification": true, + "results": { + "original": { + "ordinaryAfterCompletion": 0, + "cancelledPrompts": 32, + "sizesAfterEviction": { + "prompts": 128, + "journalBindings": 256 + }, + "afterEvictionBeforeCompletion": 32, + "afterExactTurnCompletion": 32, + "afterAllLookupMapsEmpty": 32, + "afterSessionClear": 0, + "oldAfterReplacementCompletion": 1, + "replacementClaimPreserved": true, + "wrongThreadPreserved": true, + "wrongTurnPreserved": true, + "rejectedCompletionPreserved": true, + "successfulInterruptRequests": 34 + }, + "candidate": { + "ordinaryAfterCompletion": 0, + "cancelledPrompts": 32, + "sizesAfterEviction": { + "prompts": 128, + "journalBindings": 256 + }, + "afterEvictionBeforeCompletion": 32, + "afterExactTurnCompletion": 0, + "afterAllLookupMapsEmpty": 0, + "afterSessionClear": 0, + "oldAfterReplacementCompletion": 0, + "replacementClaimPreserved": true, + "wrongThreadPreserved": true, + "wrongTurnPreserved": true, + "rejectedCompletionPreserved": true, + "successfulInterruptRequests": 34 + } + }, + "versions": { + "original": { + "bundleSha256": "e060beb8d88044d6abe07134880d9ddcbb8487464c963d0cce336454e789bcc8", + "dependencies": [ + { + "path": "src/main/native-chat/agent-session-journal/journal-payload-bounds.ts", + "sha256": "a421da6c6eee9346746a69f8ba7ff966ce81969dfa0a93f58784cac27cf8cdad" + }, + { + "path": "src/shared/agent-session-wire-refusals.ts", + "sha256": "25123cce70418deb3d2f75cf5c3b7431aa6a8c3b834f022cd697230345c595ea" + }, + { + "path": "src/shared/agent-session-background-task-wire.ts", + "sha256": "6a756544bb07864d5a6e4573992f4b3bb0d0788acb686c16da177095be23167c" + }, + { + "path": "src/shared/agent-session-wire.ts", + "sha256": "466b641465f92957dafe873c68d4cb42f50aeb0768a111397050b50aa96ac64f" + }, + { + "path": "src/main/codex/codex-prompt-registry-bounds.ts", + "sha256": "f6bfd21bcc2ccf7005a93a4764d17235887f11380b0b29301410b0f291a5c618" + }, + { + "path": "src/main/codex/codex-item-field-readers.ts", + "sha256": "aa4b178c562a63995b7de63c6f5daaf7b6b610fb163fb0a357cc230314076323" + }, + { + "path": "src/main/codex/codex-prompt-registry.ts", + "sha256": "254c530216f47abfc3572bb1689161ad3c10775af2cdceda7e94f443e52c0691" + }, + { + "path": "src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts", + "sha256": "9a7bb24e419dbd41f4aaaa20b87dd82859579d663ebb800fc605df4b471749ef" + }, + { + "path": "src/main/codex/codex-structured-prompt-replies.ts", + "sha256": "7b927e9910031ee80ab885ce1d713133d3ee614c5b6d460a5c91ff3768d9cb2e" + }, + { + "path": "src/shared/child-process/cancel-process-acquisition.ts", + "sha256": "709a04316da5f3528e33e02bbb6f054012714efc73628a04fd08289eae0c42d5" + }, + { + "path": "src/main/codex/codex-structured-acquisition-window.ts", + "sha256": "6828970d450a53fcb30ecc0ecb20d8452395bf579cc2295ee53415c299d039a4" + }, + { + "path": "src/main/codex/codex-structured-session-state.ts", + "sha256": "271837e87efac55238342a26ccdf350d9eb663b1d38a2553886429d1376ab1f4" + }, + { + "path": "src/main/codex/codex-structured-prompt-ownership.ts", + "sha256": "8d86f3784b79a1470e4002527bdfa505efe2762cba865de7716442f7d9730013" + }, + { + "path": "src/shared/child-process/windows-command-line.ts", + "sha256": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + }, + { + "path": "src/shared/child-process/windows-cmd-shim-resolution.ts", + "sha256": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + }, + { + "path": "src/shared/child-process/spawn-resolution.ts", + "sha256": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + }, + { + "path": "src/shared/child-process/process-tree-kill-gate.ts", + "sha256": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + }, + { + "path": "src/shared/child-process/process-tree-termination.ts", + "sha256": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + }, + { + "path": "src/shared/child-process/bounded-output-sink.ts", + "sha256": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + }, + { + "path": "src/shared/child-process/child-termination-reporter.ts", + "sha256": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + }, + { + "path": "src/shared/child-process/process-spec.ts", + "sha256": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + }, + { + "path": "src/shared/child-process/run-process.ts", + "sha256": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + }, + { + "path": "src/shared/child-process/retryable-process-exit-proof.ts", + "sha256": "411a39e4508763d8c4ef331bd16e88c1fe497f03188493241afcce6eab0a6e65" + }, + { + "path": "src/main/codex/codex-app-server-posix-supervisor.ts", + "sha256": "ccd4ff7a43d3ba92b9becab45304d5f5522f5c1d5aa53d379fdd47845d0fe545" + }, + { + "path": "src/main/codex/codex-app-server-capability-signal.ts", + "sha256": "43cf352901aeff4ed9143f51f224770f3dc92bf79dcfac6875bbc64105184c2f" + }, + { + "path": "src/main/codex/codex-process-exit-deadline.ts", + "sha256": "952cbb7c8778b8187ff443f8c045be53c14c3778af1b2ec77a3af5cd3ff13364" + }, + { + "path": "src/shared/system-cli-install-dirs.ts", + "sha256": "635b59f51022c9f31bf7720385b343018d4ce849220d422115e74f5351a9f21d" + }, + { + "path": "src/shared/node-cli-command-resolution.ts", + "sha256": "5eecee0c0d6296b962315c2ef2ae228a1bb1b6a7449f46d5afe3977f0af1e145" + }, + { + "path": "src/main/codex/codex-app-server-process-tree-kill.ts", + "sha256": "0a370b1d40509aa967634d6551f6baf5462b648910ddb79c2624555a84116b0b" + }, + { + "path": "src/shared/main-process-ndjson-framer.ts", + "sha256": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490" + }, + { + "path": "src/main/codex/codex-app-server-record-reader.ts", + "sha256": "b4d600293228ae2429090facc3db6f98dff449f2c3d82f35a0bea11fe4d70048" + }, + { + "path": "src/main/codex/codex-app-server-session.ts", + "sha256": "9adfbab8e229d3998d973ad9abfe647015ff21a5517fa6e5c7b32acf0a446eb8" + }, + { + "path": "src/main/codex/codex-app-server-exit-error.ts", + "sha256": "d58cdd97ae3ad4a786c7c4a0b1062ee3c5bb15b0943046dda7fff4b51ede21d1" + }, + { + "path": "src/main/codex/codex-app-server-handshake.ts", + "sha256": "3dc86d5961076713cb67ff58f593ca0fe848cd2f6b5a3a0a5da83773f49e6516" + }, + { + "path": "src/main/codex/codex-app-server-handshake-exit-proof.ts", + "sha256": "01832f6ab97bb68566221151d6a88e03cd5461f699991ad0e2c1f52573541ca1" + }, + { + "path": "src/shared/crash-reporting-diagnostic-bundle.ts", + "sha256": "2ac76a8941d4e8ae8d4e0a95ac9e10102602670e4c8fdbcb88a03b014903d1ba" + }, + { + "path": "src/shared/crash-report-signature-lines.ts", + "sha256": "564936f235f0c9faefc0c9f18f2fcbc588e5bef69ecd1071baac5a30a7c662a8" + }, + { + "path": "src/shared/posix-wait-status.ts", + "sha256": "1eb72525881baa2815548d31d2b83aaecf88e5dfd0258dbcaa5cda1e8fdcd69f" + }, + { + "path": "src/shared/crash-report-exit-code.ts", + "sha256": "a73f4e6fce72f11f5eec32c2a22d2e749f330f434aef20cec6dc03dbde55ebc6" + }, + { + "path": "src/shared/react-update-depth-attribution.ts", + "sha256": "15fd538fa4953088b63d9307fc575d4f795614b445a558340d80d61bb4c50929" + }, + { + "path": "src/shared/crash-report-attribution-lines.ts", + "sha256": "3db6cabca0444343347a90e6ec71a015a79489097e2484202cf2b57232483a6e" + }, + { + "path": "src/shared/crash-report-redaction.ts", + "sha256": "afd00bd49c77398425ee481070818e98067e90bb472a7454c571bf77ceee9c7f" + }, + { + "path": "src/shared/crash-reporting.ts", + "sha256": "e9749318038d145b7ae1819ffed6b285815c502d0eba13181c48b73b1bd3e918" + }, + { + "path": "src/main/observability/redactor.ts", + "sha256": "59190309dbef22508e3f2ef27b0190093d5aaab8cce146d6d49a89eb9d0a2b73" + }, + { + "path": "src/main/observability/tracer.ts", + "sha256": "82b6f4ec0be07b6aa612038b21b9f67b0ef199f694d29dc9f7b54b55dba9776e" + }, + { + "path": "src/main/crash-reporting/crash-breadcrumb-store.ts", + "sha256": "8848be8a897e0d1a214dbbaf87a0377d493ee83358763c0fc286e693889b2cb9" + }, + { + "path": "src/main/crash-reporting/main-process-lifecycle-identity.ts", + "sha256": "797cd37e5af6ec71f91636bc779274572a62ea67e5fa2874bb1f2f771020c950" + }, + { + "path": "src/main/crash-reporting/durable-crash-breadcrumb.ts", + "sha256": "477e28157e119f3bd85bc77d4adb185d36e44103b8a087d680715ce71b05dea7" + }, + { + "path": "src/main/crash-reporting/self-initiated-tree-kill-log.ts", + "sha256": "eb28b4a513a3cbc30586df30569adf29d828f059522ebe6c751d55b233c13987" + }, + { + "path": "src/shared/app-environment.ts", + "sha256": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140" + }, + { + "path": "src/main/orca-chromium-process-pids.ts", + "sha256": "960a15f005ff997fa379972a8c2abe3410b78c6facee449e8a5350754cfce965" + }, + { + "path": "src/main/own-chromium-tree-kill-guard.ts", + "sha256": "3a59cccd8cf923506aacad58f1c5d11b49f47a21e6c6dee8559912ee3c5cefb5" + }, + { + "path": "src/main/windows-process-tree-kill.ts", + "sha256": "83236378e93e3a15189ca02febf6b9b94d9d7eadafb46f0fcb72051904e6f45d" + }, + { + "path": "src/shared/process-table-index.ts", + "sha256": "30f236b7d0275f05673c0a7982bb0aa67c9319762749f5a583c5c31e51e31f36" + }, + { + "path": "src/shared/process-table-snapshot.ts", + "sha256": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + }, + { + "path": "src/shared/process-table-snapshot-reader.ts", + "sha256": "7d35601a2a7bf7a9c5b4a897995e0e8a15775b4d35939f3dd1abe1a042bb1c5e" + }, + { + "path": "src/main/windows/windows-command-line-recovery-health.ts", + "sha256": "a135da3611d54fecb0dba52b4d81e06ca1532680424b55e7efa315b7c886ec33" + }, + { + "path": "src/shared/child-process/windows-system-binary.ts", + "sha256": "0c42c7c454534a0937f4153e9708aeb243bd930d130ffc6493ac42d21405606e" + }, + { + "path": "src/main/windows/windows-process-table-cim-scan.ts", + "sha256": "ecfa487a3139cc087eae78ff61bf358f7eb62aec3b20e9d064d96b6e37862ab0" + }, + { + "path": "src/main/windows/windows-process-table.ts", + "sha256": "de0565f724f49ca393bb55c1618e697ea34e98bfee132c196d0c3e97713aabe1" + }, + { + "path": "src/main/providers/windows-foreground-process-rows.ts", + "sha256": "374accddf905f5b60fb1be4010e8160bfaf7f1a3834ecaffbf0119e555c7a765" + }, + { + "path": "src/main/windows-pty-root-identity.ts", + "sha256": "5e1ec017199f1a78150064d22eca4fd724874473c042f78fa236f3831f761711" + }, + { + "path": "src/main/pty-descendant-termination.ts", + "sha256": "dbdfe91d9248b4fbfbdac1733237ff3a4776e596a6a127fbdf5c4fb1d987b61e" + }, + { + "path": "src/main/pty-descendant-exit-verification.ts", + "sha256": "1f7a14a8273a228180538b3d37a20e0631e60a2e497134de499907eabfeb9b77" + }, + { + "path": "src/main/runtime/agent-session-process-identity-probe.ts", + "sha256": "4f6bc0fc286f74b07a26bdfbe69a9124498deb289c5d96db8b1334e12fdcce33" + }, + { + "path": "src/main/codex/codex-structured-owner-identity.ts", + "sha256": "b5b286f638515cae36539d0061c3808e64e6b2ed464f9326255fcd2f655d85f7" + }, + { + "path": "src/main/runtime/agent-session-spawn-token-readback.ts", + "sha256": "1b2481a5487b9ee7e21a2103b468e69d91879f84a2a330b7fa81c7a3b056e399" + }, + { + "path": "src/main/runtime/agent-session-spawn-token-process-scan.ts", + "sha256": "57fe5ac196d53e266b4d5e9ea0fab7311a292b5f281cbe56eea98c5579634bfc" + }, + { + "path": "src/main/codex/codex-app-server-process-teardown.ts", + "sha256": "183110df0c529c8eaec77ed457a9b17c5322d313482119f488f019dbf6d73f75" + }, + { + "path": "src/main/codex/codex-app-server-frame-size-error.ts", + "sha256": "8f567def6e41edb29c3a56b2be4377f64a99999c76bf24be2980a296ce6c01e5" + }, + { + "path": "src/main/codex/codex-app-server-jsonl.ts", + "sha256": "428b802a6704ec57efb26c696ad9e77da727a990b881665fdf5f68542cf143b4" + }, + { + "path": "src/main/codex/codex-app-server-request-error.ts", + "sha256": "67140be11e2b29f93d8a352552af733d228ac1808b6cf53bb7227e3906794eea" + }, + { + "path": "src/main/codex/codex-app-server-record-prefix.ts", + "sha256": "e70f8492995568e2891af0f58a78e5eb9a2f5f1970b9a561aa1bd7af32673f0d" + }, + { + "path": "src/main/codex/codex-app-server-record-dispatch.ts", + "sha256": "48554c6f9d4cb056d1a8e2e495b1c3fdadecf891d8f02d9599a7af3989f6ab23" + }, + { + "path": "src/main/codex/codex-app-server-connection.ts", + "sha256": "30fcca9f4cda5d6cceb9215905d7f3ead2361764b80e0e67ba0bcbb55e00672d" + }, + { + "path": "src/main/codex/codex-structured-thread-facts.ts", + "sha256": "a244c847c5ca22c3e3fac3b075b51f0a566f8cf6726731f69f50f7eb079b063a" + }, + { + "path": "src/main/codex/codex-structured-turn-processes.ts", + "sha256": "6a942ed29995d8001f8c041c693ed7bd30ecfc3fac5dd30e4c5ec70c0a12396a" + }, + { + "path": "src/main/codex/codex-structured-turn-cancellation.ts", + "sha256": "518b8e5a2f6dc4f8e46e01366c0a61830d6c805b68101484726bd62bbfc831a8" + }, + { + "path": "src/shared/orca-dispatch-status-prompt.ts", + "sha256": "fd9fedb3c839cd9d17b545329937c8fb6b9fa081120eb4678a9fb66f4252629e" + }, + { + "path": "src/shared/agent-status-field-normalization.ts", + "sha256": "a60c119eade7026c9e6f37ff98e77e718bd0cb41e929b0e256872981411e3328" + }, + { + "path": "src/main/native-chat/agent-session-wire/provider-frame-activity.ts", + "sha256": "9888661f598cee4d622f22b208f9ba1fc6615df6424328cf818cebe9fe86b8d7" + }, + { + "path": "src/main/codex/codex-subagent-activity.ts", + "sha256": "5cdf18a4ea2c68178a67a845d4c95672a0d94bb282e8e15c3a930c6dd59151e4" + }, + { + "path": "src/shared/native-chat-types.ts", + "sha256": "ca9440aba61822a8b92cc1009790d5f6e7bdb2f570cf159349a42027dfd40def" + }, + { + "path": "src/shared/native-chat-subagent-summary.ts", + "sha256": "a2cdf19a07edf3b50a971af22044608d50ea9cfebd07abc01af38755a2143077" + }, + { + "path": "src/main/codex/codex-subagent-executions.ts", + "sha256": "2a356e9a108a302e1131d40112401a48c582b2d5560eab073004a7efefde5259" + }, + { + "path": "src/main/codex/codex-subagent-group-body.ts", + "sha256": "9ff41e055ee33fb556f2079604271f13e696ca0114eaaa9a5f25166e5fa49280" + }, + { + "path": "src/main/codex/codex-structured-journal-limits.ts", + "sha256": "2fd2d5015b73fb948df6abdb6469039b1ee3a680db5119b1a44ebf82bc152492" + }, + { + "path": "src/main/codex/codex-subagent-roster.ts", + "sha256": "bafd291665fb178b0912bd2a2723910c00890f0766aae7f70f8bed5d417542b1" + }, + { + "path": "src/shared/native-chat-turn-status.ts", + "sha256": "4ce2b64fa7818106814c135e5daf77bb183f601c35f9f08e5b7ff847fba01378" + }, + { + "path": "src/shared/native-chat-tool-identity.ts", + "sha256": "8c98eec1a53b26a67ed48a0859c5863f5accbe67e013c4eb5a0f8b32441ee925" + }, + { + "path": "src/main/codex/codex-goal-journal-rows.ts", + "sha256": "415846ca105300349778da7637216ca6ce978c7036219f6b3038f7228ba8724e" + }, + { + "path": "src/main/native-chat/agent-session-wire/provider-frame-disposition.ts", + "sha256": "f24008cebb1b8a77b0cbd091e49140a4bf99c676bc61eedeb0a82f1263244a7c" + }, + { + "path": "src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts", + "sha256": "f8169a024a573adfc97e4eb8c99ecc4e80f82e9f94a04a29011a6b8051cc7626" + }, + { + "path": "src/shared/raster-image-dimensions.ts", + "sha256": "0d7462b8b2e53bfcdcbd32803edcfb85ed0063f56d45cf230fb59aac4783d063" + }, + { + "path": "src/shared/raster-image-preview-limits.ts", + "sha256": "f2d836b354b3951912f1ec689d84b82aa1aade89ed8e9c6f76be2321d600e9a7" + }, + { + "path": "src/shared/raster-image-base64-preview.ts", + "sha256": "89baf40f4637615456888e0f35115517a8445a3b1cb640b7bc495dd9a98fcedb" + }, + { + "path": "src/shared/image-data-uri.ts", + "sha256": "3eb9d2bda499b8f10c74783f9de766041469b140a9b96b2b5be7d97528688ce0" + }, + { + "path": "src/main/codex/codex-image-item-translation.ts", + "sha256": "c4f88236c707472cbcfcdfb1a04e51c738f8741aa5847a33f730759499522f5e" + }, + { + "path": "src/main/codex/codex-command-action-class.ts", + "sha256": "93b7bceb66a9e5bfae680e018b61bddb5c1117fe26243088ceccc8738841f966" + }, + { + "path": "src/main/codex/codex-thread-item-identity.ts", + "sha256": "04b98b367a4d658fb759ebc4fc73ea4ae731ae3f238f191c8ce002b2a2c9e2a8" + }, + { + "path": "src/main/codex/codex-turn-ordinals.ts", + "sha256": "9b7cf66986235bcacd6c198950452c7f40bd559a3a0ce20995aac496a1e32d7f" + }, + { + "path": "src/main/codex/codex-structured-item-translation.ts", + "sha256": "8ab9debb279baf3a5043ffe754c8911755c6e4ca9edfc0a094671c4d5d166639" + }, + { + "path": "src/main/codex/codex-structured-journal-contracts.ts", + "sha256": "59dbe0f4f38aea3ff31ae62d7a8d244636e2813bb4a0f1e1c2db3362bdf193ac" + }, + { + "path": "src/main/codex/codex-structured-journal-generic-frames.ts", + "sha256": "5fe2414993c44075d9c979d76091c89611360cb166f650e4483d1e6bad3b30d2" + }, + { + "path": "src/main/native-chat/agent-session-wire/structured-session-compaction.ts", + "sha256": "c73d272d59d71522d3d61eea66cf61da205c739743d720d15cb70f78bd5e07ac" + }, + { + "path": "src/shared/agent-session-journal-item-key.ts", + "sha256": "09ebe4758e3f38b5b7591f07f7b4dbfd04c7cc01713c9b0afe03b07b656d1fe8" + }, + { + "path": "src/shared/agent-session-journal-types.ts", + "sha256": "4f85b301aa12ea14ac87b0099135247ad0758529cdb2ce003bb6a1031cf279ea" + }, + { + "path": "src/shared/agent-session-journal-schemas.ts", + "sha256": "579ab7671b7f8e46374f11b6ac669bf659bd5505345c49d7dd1c9acc4b0ef564" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-row-schema.ts", + "sha256": "de9c4f6324feef96fd33cfb0698f4c34d7e02694809fb133757146ead0cbb9c3" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.ts", + "sha256": "9361c2255ac501b4c18baf22dd4e74936bc8c1181f227329b97bf1af7593bf4c" + }, + { + "path": "src/main/codex/codex-structured-journal-sink.ts", + "sha256": "e05226f2cb906557a0811de780b9f30e4bbedbd6253cd4948e5518646b63cf26" + }, + { + "path": "src/main/codex/codex-structured-journal-compactions.ts", + "sha256": "45c8e450be19cf8b88b5ffa90311e80390f40a32fd2ceaddf6bfd4cf7992ba03" + }, + { + "path": "src/main/codex/codex-goal-journal-identity.ts", + "sha256": "55d5c3a3120ea038aac9ae64cd51c5bfdf0e13af1306dca01009993cf482ad08" + }, + { + "path": "src/main/codex/codex-structured-journal-goals.ts", + "sha256": "d14794482031e86cf21f2c859ce8097f0759a5a0b2bc8f97ea26598ecfd190a5" + }, + { + "path": "src/shared/agent-turn-lifecycle-text.ts", + "sha256": "bf3a91d9c5157be19c9c88aed96e2c4e076e744362b34ddf2b466c707067e32a" + }, + { + "path": "src/shared/agent-session-turn-record.ts", + "sha256": "1a3df88a627b4744ccbe8168860033dfef66bd4ff028c6431af6a6ac2f63194f" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-terminal-settlement.ts", + "sha256": "9344b1593bf91a49a6e1d2fb9604e2890939eb1b1f0cadbbdae7194db57a8080" + }, + { + "path": "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts", + "sha256": "48828e4ee21858075cbb87ec1caa4a82991a55f80928482615d1df7ec5dc0fb2" + }, + { + "path": "src/main/codex/codex-command-lifecycle.ts", + "sha256": "7b480d3b111304e3caff71768fb3b11ab6c36e1fa8f5b0440623a77cac2382c2" + }, + { + "path": "src/main/codex/codex-structured-item-stream-bounds.ts", + "sha256": "88e9efa8a9c163905f1a5eb5eaf316fba2f194d76c5a94ed7f55636cc0d74ae0" + }, + { + "path": "src/main/codex/codex-item-stream-retention.ts", + "sha256": "66d1f1d51ed6711e20f81698b14be2d003e899dbc587ac1306b6a656cf951b96" + }, + { + "path": "src/main/codex/codex-structured-item-stream-events.ts", + "sha256": "da2a9da9025af354ce705a190badadf822f9ac3b403eef04aa0ef8aaf6f023de" + }, + { + "path": "src/main/codex/codex-structured-item-streams.ts", + "sha256": "b70c3306e8bec3587342afc3bc156f40903513fa7ff3865b217ba4cc912cd89f" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-values.ts", + "sha256": "fb66d47e32f4040f1b248a886b077506d23d0492264412e6cc3c96b579de425d" + }, + { + "path": "src/main/codex/codex-structured-dispatch-echo.ts", + "sha256": "4be5e3b55bd0a5507878798f221e758444dc82ede2f14e741f08b6b85979cd7a" + }, + { + "path": "src/main/codex/codex-structured-journal-items.ts", + "sha256": "7a9dc981cdcb1931d620feaa5523b486600c50c241f223ffdc71aa9edeb4a5bb" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts", + "sha256": "e4382b9d5b8a9cdba94b2ee71188119790987b11fd0f8f9718ff4be899e9a9fa" + }, + { + "path": "src/main/codex/codex-structured-prompt-items.ts", + "sha256": "813e8fdf2768663e31822699a5a7311df8e714ca2a269dcbb1f42e8d196ac456" + }, + { + "path": "src/main/codex/codex-structured-journal-prompts.ts", + "sha256": "806f61f913c28593b02b6ea6dcb68559b126bf939b91b1e5cf4f43fedabf45a2" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turns.ts", + "sha256": "9042b2393d7d8898ed4f7436669dfbc82504e15b9ea90eb00e1518ee4db254e0" + }, + { + "path": "src/main/codex/codex-structured-journal-settlement.ts", + "sha256": "761616c292e15894b337fdd171acd7c2c1272fcabdbad3ea460f8831a923c797" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-frames.ts", + "sha256": "0679aea6b6fc274448d007c292218f90bfdd8ad3ed4ce4a7d59d0a15aae9e455" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-restore.ts", + "sha256": "a2e521b7cae677a5b3fd671065e5e4b1c812e2444f272df0f79feddae3640652" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turn-boundaries.ts", + "sha256": "bb482b2c49fa3754b76e1e749185bb2804409e017f909c0453022ead07f99a03" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turn-state.ts", + "sha256": "f6dd637f0e77f193923c3f3843b86d12786b75b9ce76e5a7e057a7187a832fe5" + }, + { + "path": "src/main/codex/codex-structured-journal-translation.ts", + "sha256": "56e17b9649471e804d29d1d54a3e14b4298ca8af2e25c4759dc37fce000b06a1" + }, + { + "path": "src/main/codex/codex-server-request-disposition.ts", + "sha256": "ddd362e00a38ff0166d35bc4cca51e4f79e377f188a9e07f5966da7c056854a0" + }, + { + "path": "src/shared/node-bounded-json-stringify.ts", + "sha256": "fcadabc537aa3125d1ee514026ecca5b8e2a76aabca7e29f08d3d35008001622" + }, + { + "path": "src/shared/remote-runtime-client-error.ts", + "sha256": "593ff4ee6466c4b18b69f65a20631967205fb8e1bd5e46239cf7f526b764156a" + }, + { + "path": "src/shared/remote-runtime-memory-limits.ts", + "sha256": "862a317d7f564a2cc54cfa60b6c093de5dc8a61714eac45daa178608c1f6acb1" + }, + { + "path": "src/main/native-chat/agent-session-wire/agent-session-history-page-bounds.ts", + "sha256": "57e49a0ea59cdccf4a4fc1532d43d2c127b077233602139b2d36d79fd547fc9d" + }, + { + "path": "src/main/codex/codex-structured-rewind.ts", + "sha256": "710115f53bc673f7d7259ec9e1d6b02e7827c348225c1e6a2006ed5eaf06e6bc" + }, + { + "path": "src/main/codex/codex-structured-provider-events.ts", + "sha256": "a411dc378e521ea8fc915e2a2a21476341a30f440e8233ae5649205d2a1e2e44" + } + ] + }, + "candidate": { + "bundleSha256": "6b7ec91709cad63ae089187e297914243757d59991842be0c0c2eb92514b3548", + "dependencies": [ + { + "path": "src/main/native-chat/agent-session-journal/journal-payload-bounds.ts", + "sha256": "a421da6c6eee9346746a69f8ba7ff966ce81969dfa0a93f58784cac27cf8cdad" + }, + { + "path": "src/shared/agent-session-wire-refusals.ts", + "sha256": "25123cce70418deb3d2f75cf5c3b7431aa6a8c3b834f022cd697230345c595ea" + }, + { + "path": "src/shared/agent-session-background-task-wire.ts", + "sha256": "6a756544bb07864d5a6e4573992f4b3bb0d0788acb686c16da177095be23167c" + }, + { + "path": "src/shared/agent-session-wire.ts", + "sha256": "466b641465f92957dafe873c68d4cb42f50aeb0768a111397050b50aa96ac64f" + }, + { + "path": "src/main/codex/codex-prompt-registry-bounds.ts", + "sha256": "f6bfd21bcc2ccf7005a93a4764d17235887f11380b0b29301410b0f291a5c618" + }, + { + "path": "src/main/codex/codex-item-field-readers.ts", + "sha256": "aa4b178c562a63995b7de63c6f5daaf7b6b610fb163fb0a357cc230314076323" + }, + { + "path": "src/main/codex/codex-prompt-registry.ts", + "sha256": "3d33f98215b9547d661c0a4f6aa8b9a8a6a88fa953d90652cb3fb9953425a539" + }, + { + "path": "src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts", + "sha256": "9a7bb24e419dbd41f4aaaa20b87dd82859579d663ebb800fc605df4b471749ef" + }, + { + "path": "src/main/codex/codex-structured-prompt-replies.ts", + "sha256": "7b927e9910031ee80ab885ce1d713133d3ee614c5b6d460a5c91ff3768d9cb2e" + }, + { + "path": "src/shared/child-process/cancel-process-acquisition.ts", + "sha256": "709a04316da5f3528e33e02bbb6f054012714efc73628a04fd08289eae0c42d5" + }, + { + "path": "src/main/codex/codex-structured-acquisition-window.ts", + "sha256": "6828970d450a53fcb30ecc0ecb20d8452395bf579cc2295ee53415c299d039a4" + }, + { + "path": "src/main/codex/codex-structured-session-state.ts", + "sha256": "271837e87efac55238342a26ccdf350d9eb663b1d38a2553886429d1376ab1f4" + }, + { + "path": "src/main/codex/codex-structured-prompt-ownership.ts", + "sha256": "8d86f3784b79a1470e4002527bdfa505efe2762cba865de7716442f7d9730013" + }, + { + "path": "src/shared/child-process/windows-command-line.ts", + "sha256": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + }, + { + "path": "src/shared/child-process/windows-cmd-shim-resolution.ts", + "sha256": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + }, + { + "path": "src/shared/child-process/spawn-resolution.ts", + "sha256": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + }, + { + "path": "src/shared/child-process/process-tree-kill-gate.ts", + "sha256": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + }, + { + "path": "src/shared/child-process/process-tree-termination.ts", + "sha256": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + }, + { + "path": "src/shared/child-process/bounded-output-sink.ts", + "sha256": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + }, + { + "path": "src/shared/child-process/child-termination-reporter.ts", + "sha256": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + }, + { + "path": "src/shared/child-process/process-spec.ts", + "sha256": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + }, + { + "path": "src/shared/child-process/run-process.ts", + "sha256": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + }, + { + "path": "src/shared/child-process/retryable-process-exit-proof.ts", + "sha256": "411a39e4508763d8c4ef331bd16e88c1fe497f03188493241afcce6eab0a6e65" + }, + { + "path": "src/main/codex/codex-app-server-posix-supervisor.ts", + "sha256": "ccd4ff7a43d3ba92b9becab45304d5f5522f5c1d5aa53d379fdd47845d0fe545" + }, + { + "path": "src/main/codex/codex-app-server-capability-signal.ts", + "sha256": "43cf352901aeff4ed9143f51f224770f3dc92bf79dcfac6875bbc64105184c2f" + }, + { + "path": "src/main/codex/codex-process-exit-deadline.ts", + "sha256": "952cbb7c8778b8187ff443f8c045be53c14c3778af1b2ec77a3af5cd3ff13364" + }, + { + "path": "src/shared/system-cli-install-dirs.ts", + "sha256": "635b59f51022c9f31bf7720385b343018d4ce849220d422115e74f5351a9f21d" + }, + { + "path": "src/shared/node-cli-command-resolution.ts", + "sha256": "5eecee0c0d6296b962315c2ef2ae228a1bb1b6a7449f46d5afe3977f0af1e145" + }, + { + "path": "src/main/codex/codex-app-server-process-tree-kill.ts", + "sha256": "0a370b1d40509aa967634d6551f6baf5462b648910ddb79c2624555a84116b0b" + }, + { + "path": "src/shared/main-process-ndjson-framer.ts", + "sha256": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490" + }, + { + "path": "src/main/codex/codex-app-server-record-reader.ts", + "sha256": "b4d600293228ae2429090facc3db6f98dff449f2c3d82f35a0bea11fe4d70048" + }, + { + "path": "src/main/codex/codex-app-server-session.ts", + "sha256": "9adfbab8e229d3998d973ad9abfe647015ff21a5517fa6e5c7b32acf0a446eb8" + }, + { + "path": "src/main/codex/codex-app-server-exit-error.ts", + "sha256": "d58cdd97ae3ad4a786c7c4a0b1062ee3c5bb15b0943046dda7fff4b51ede21d1" + }, + { + "path": "src/main/codex/codex-app-server-handshake.ts", + "sha256": "3dc86d5961076713cb67ff58f593ca0fe848cd2f6b5a3a0a5da83773f49e6516" + }, + { + "path": "src/main/codex/codex-app-server-handshake-exit-proof.ts", + "sha256": "01832f6ab97bb68566221151d6a88e03cd5461f699991ad0e2c1f52573541ca1" + }, + { + "path": "src/shared/crash-reporting-diagnostic-bundle.ts", + "sha256": "2ac76a8941d4e8ae8d4e0a95ac9e10102602670e4c8fdbcb88a03b014903d1ba" + }, + { + "path": "src/shared/crash-report-signature-lines.ts", + "sha256": "564936f235f0c9faefc0c9f18f2fcbc588e5bef69ecd1071baac5a30a7c662a8" + }, + { + "path": "src/shared/posix-wait-status.ts", + "sha256": "1eb72525881baa2815548d31d2b83aaecf88e5dfd0258dbcaa5cda1e8fdcd69f" + }, + { + "path": "src/shared/crash-report-exit-code.ts", + "sha256": "a73f4e6fce72f11f5eec32c2a22d2e749f330f434aef20cec6dc03dbde55ebc6" + }, + { + "path": "src/shared/react-update-depth-attribution.ts", + "sha256": "15fd538fa4953088b63d9307fc575d4f795614b445a558340d80d61bb4c50929" + }, + { + "path": "src/shared/crash-report-attribution-lines.ts", + "sha256": "3db6cabca0444343347a90e6ec71a015a79489097e2484202cf2b57232483a6e" + }, + { + "path": "src/shared/crash-report-redaction.ts", + "sha256": "afd00bd49c77398425ee481070818e98067e90bb472a7454c571bf77ceee9c7f" + }, + { + "path": "src/shared/crash-reporting.ts", + "sha256": "e9749318038d145b7ae1819ffed6b285815c502d0eba13181c48b73b1bd3e918" + }, + { + "path": "src/main/observability/redactor.ts", + "sha256": "59190309dbef22508e3f2ef27b0190093d5aaab8cce146d6d49a89eb9d0a2b73" + }, + { + "path": "src/main/observability/tracer.ts", + "sha256": "82b6f4ec0be07b6aa612038b21b9f67b0ef199f694d29dc9f7b54b55dba9776e" + }, + { + "path": "src/main/crash-reporting/crash-breadcrumb-store.ts", + "sha256": "8848be8a897e0d1a214dbbaf87a0377d493ee83358763c0fc286e693889b2cb9" + }, + { + "path": "src/main/crash-reporting/main-process-lifecycle-identity.ts", + "sha256": "797cd37e5af6ec71f91636bc779274572a62ea67e5fa2874bb1f2f771020c950" + }, + { + "path": "src/main/crash-reporting/durable-crash-breadcrumb.ts", + "sha256": "477e28157e119f3bd85bc77d4adb185d36e44103b8a087d680715ce71b05dea7" + }, + { + "path": "src/main/crash-reporting/self-initiated-tree-kill-log.ts", + "sha256": "eb28b4a513a3cbc30586df30569adf29d828f059522ebe6c751d55b233c13987" + }, + { + "path": "src/shared/app-environment.ts", + "sha256": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140" + }, + { + "path": "src/main/orca-chromium-process-pids.ts", + "sha256": "960a15f005ff997fa379972a8c2abe3410b78c6facee449e8a5350754cfce965" + }, + { + "path": "src/main/own-chromium-tree-kill-guard.ts", + "sha256": "3a59cccd8cf923506aacad58f1c5d11b49f47a21e6c6dee8559912ee3c5cefb5" + }, + { + "path": "src/main/windows-process-tree-kill.ts", + "sha256": "83236378e93e3a15189ca02febf6b9b94d9d7eadafb46f0fcb72051904e6f45d" + }, + { + "path": "src/shared/process-table-index.ts", + "sha256": "30f236b7d0275f05673c0a7982bb0aa67c9319762749f5a583c5c31e51e31f36" + }, + { + "path": "src/shared/process-table-snapshot.ts", + "sha256": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + }, + { + "path": "src/shared/process-table-snapshot-reader.ts", + "sha256": "7d35601a2a7bf7a9c5b4a897995e0e8a15775b4d35939f3dd1abe1a042bb1c5e" + }, + { + "path": "src/main/windows/windows-command-line-recovery-health.ts", + "sha256": "a135da3611d54fecb0dba52b4d81e06ca1532680424b55e7efa315b7c886ec33" + }, + { + "path": "src/shared/child-process/windows-system-binary.ts", + "sha256": "0c42c7c454534a0937f4153e9708aeb243bd930d130ffc6493ac42d21405606e" + }, + { + "path": "src/main/windows/windows-process-table-cim-scan.ts", + "sha256": "ecfa487a3139cc087eae78ff61bf358f7eb62aec3b20e9d064d96b6e37862ab0" + }, + { + "path": "src/main/windows/windows-process-table.ts", + "sha256": "de0565f724f49ca393bb55c1618e697ea34e98bfee132c196d0c3e97713aabe1" + }, + { + "path": "src/main/providers/windows-foreground-process-rows.ts", + "sha256": "374accddf905f5b60fb1be4010e8160bfaf7f1a3834ecaffbf0119e555c7a765" + }, + { + "path": "src/main/windows-pty-root-identity.ts", + "sha256": "5e1ec017199f1a78150064d22eca4fd724874473c042f78fa236f3831f761711" + }, + { + "path": "src/main/pty-descendant-termination.ts", + "sha256": "dbdfe91d9248b4fbfbdac1733237ff3a4776e596a6a127fbdf5c4fb1d987b61e" + }, + { + "path": "src/main/pty-descendant-exit-verification.ts", + "sha256": "1f7a14a8273a228180538b3d37a20e0631e60a2e497134de499907eabfeb9b77" + }, + { + "path": "src/main/runtime/agent-session-process-identity-probe.ts", + "sha256": "4f6bc0fc286f74b07a26bdfbe69a9124498deb289c5d96db8b1334e12fdcce33" + }, + { + "path": "src/main/codex/codex-structured-owner-identity.ts", + "sha256": "b5b286f638515cae36539d0061c3808e64e6b2ed464f9326255fcd2f655d85f7" + }, + { + "path": "src/main/runtime/agent-session-spawn-token-readback.ts", + "sha256": "1b2481a5487b9ee7e21a2103b468e69d91879f84a2a330b7fa81c7a3b056e399" + }, + { + "path": "src/main/runtime/agent-session-spawn-token-process-scan.ts", + "sha256": "57fe5ac196d53e266b4d5e9ea0fab7311a292b5f281cbe56eea98c5579634bfc" + }, + { + "path": "src/main/codex/codex-app-server-process-teardown.ts", + "sha256": "183110df0c529c8eaec77ed457a9b17c5322d313482119f488f019dbf6d73f75" + }, + { + "path": "src/main/codex/codex-app-server-frame-size-error.ts", + "sha256": "8f567def6e41edb29c3a56b2be4377f64a99999c76bf24be2980a296ce6c01e5" + }, + { + "path": "src/main/codex/codex-app-server-jsonl.ts", + "sha256": "428b802a6704ec57efb26c696ad9e77da727a990b881665fdf5f68542cf143b4" + }, + { + "path": "src/main/codex/codex-app-server-request-error.ts", + "sha256": "67140be11e2b29f93d8a352552af733d228ac1808b6cf53bb7227e3906794eea" + }, + { + "path": "src/main/codex/codex-app-server-record-prefix.ts", + "sha256": "e70f8492995568e2891af0f58a78e5eb9a2f5f1970b9a561aa1bd7af32673f0d" + }, + { + "path": "src/main/codex/codex-app-server-record-dispatch.ts", + "sha256": "48554c6f9d4cb056d1a8e2e495b1c3fdadecf891d8f02d9599a7af3989f6ab23" + }, + { + "path": "src/main/codex/codex-app-server-connection.ts", + "sha256": "30fcca9f4cda5d6cceb9215905d7f3ead2361764b80e0e67ba0bcbb55e00672d" + }, + { + "path": "src/main/codex/codex-structured-thread-facts.ts", + "sha256": "a244c847c5ca22c3e3fac3b075b51f0a566f8cf6726731f69f50f7eb079b063a" + }, + { + "path": "src/main/codex/codex-structured-turn-processes.ts", + "sha256": "6a942ed29995d8001f8c041c693ed7bd30ecfc3fac5dd30e4c5ec70c0a12396a" + }, + { + "path": "src/main/codex/codex-structured-turn-cancellation.ts", + "sha256": "518b8e5a2f6dc4f8e46e01366c0a61830d6c805b68101484726bd62bbfc831a8" + }, + { + "path": "src/shared/orca-dispatch-status-prompt.ts", + "sha256": "fd9fedb3c839cd9d17b545329937c8fb6b9fa081120eb4678a9fb66f4252629e" + }, + { + "path": "src/shared/agent-status-field-normalization.ts", + "sha256": "a60c119eade7026c9e6f37ff98e77e718bd0cb41e929b0e256872981411e3328" + }, + { + "path": "src/main/native-chat/agent-session-wire/provider-frame-activity.ts", + "sha256": "9888661f598cee4d622f22b208f9ba1fc6615df6424328cf818cebe9fe86b8d7" + }, + { + "path": "src/main/codex/codex-subagent-activity.ts", + "sha256": "5cdf18a4ea2c68178a67a845d4c95672a0d94bb282e8e15c3a930c6dd59151e4" + }, + { + "path": "src/shared/native-chat-types.ts", + "sha256": "ca9440aba61822a8b92cc1009790d5f6e7bdb2f570cf159349a42027dfd40def" + }, + { + "path": "src/shared/native-chat-subagent-summary.ts", + "sha256": "a2cdf19a07edf3b50a971af22044608d50ea9cfebd07abc01af38755a2143077" + }, + { + "path": "src/main/codex/codex-subagent-executions.ts", + "sha256": "2a356e9a108a302e1131d40112401a48c582b2d5560eab073004a7efefde5259" + }, + { + "path": "src/main/codex/codex-subagent-group-body.ts", + "sha256": "9ff41e055ee33fb556f2079604271f13e696ca0114eaaa9a5f25166e5fa49280" + }, + { + "path": "src/main/codex/codex-structured-journal-limits.ts", + "sha256": "2fd2d5015b73fb948df6abdb6469039b1ee3a680db5119b1a44ebf82bc152492" + }, + { + "path": "src/main/codex/codex-subagent-roster.ts", + "sha256": "bafd291665fb178b0912bd2a2723910c00890f0766aae7f70f8bed5d417542b1" + }, + { + "path": "src/shared/native-chat-turn-status.ts", + "sha256": "4ce2b64fa7818106814c135e5daf77bb183f601c35f9f08e5b7ff847fba01378" + }, + { + "path": "src/shared/native-chat-tool-identity.ts", + "sha256": "8c98eec1a53b26a67ed48a0859c5863f5accbe67e013c4eb5a0f8b32441ee925" + }, + { + "path": "src/main/codex/codex-goal-journal-rows.ts", + "sha256": "415846ca105300349778da7637216ca6ce978c7036219f6b3038f7228ba8724e" + }, + { + "path": "src/main/native-chat/agent-session-wire/provider-frame-disposition.ts", + "sha256": "f24008cebb1b8a77b0cbd091e49140a4bf99c676bc61eedeb0a82f1263244a7c" + }, + { + "path": "src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts", + "sha256": "f8169a024a573adfc97e4eb8c99ecc4e80f82e9f94a04a29011a6b8051cc7626" + }, + { + "path": "src/shared/raster-image-dimensions.ts", + "sha256": "0d7462b8b2e53bfcdcbd32803edcfb85ed0063f56d45cf230fb59aac4783d063" + }, + { + "path": "src/shared/raster-image-preview-limits.ts", + "sha256": "f2d836b354b3951912f1ec689d84b82aa1aade89ed8e9c6f76be2321d600e9a7" + }, + { + "path": "src/shared/raster-image-base64-preview.ts", + "sha256": "89baf40f4637615456888e0f35115517a8445a3b1cb640b7bc495dd9a98fcedb" + }, + { + "path": "src/shared/image-data-uri.ts", + "sha256": "3eb9d2bda499b8f10c74783f9de766041469b140a9b96b2b5be7d97528688ce0" + }, + { + "path": "src/main/codex/codex-image-item-translation.ts", + "sha256": "c4f88236c707472cbcfcdfb1a04e51c738f8741aa5847a33f730759499522f5e" + }, + { + "path": "src/main/codex/codex-command-action-class.ts", + "sha256": "93b7bceb66a9e5bfae680e018b61bddb5c1117fe26243088ceccc8738841f966" + }, + { + "path": "src/main/codex/codex-thread-item-identity.ts", + "sha256": "04b98b367a4d658fb759ebc4fc73ea4ae731ae3f238f191c8ce002b2a2c9e2a8" + }, + { + "path": "src/main/codex/codex-turn-ordinals.ts", + "sha256": "9b7cf66986235bcacd6c198950452c7f40bd559a3a0ce20995aac496a1e32d7f" + }, + { + "path": "src/main/codex/codex-structured-item-translation.ts", + "sha256": "8ab9debb279baf3a5043ffe754c8911755c6e4ca9edfc0a094671c4d5d166639" + }, + { + "path": "src/main/codex/codex-structured-journal-contracts.ts", + "sha256": "59dbe0f4f38aea3ff31ae62d7a8d244636e2813bb4a0f1e1c2db3362bdf193ac" + }, + { + "path": "src/main/codex/codex-structured-journal-generic-frames.ts", + "sha256": "5fe2414993c44075d9c979d76091c89611360cb166f650e4483d1e6bad3b30d2" + }, + { + "path": "src/main/native-chat/agent-session-wire/structured-session-compaction.ts", + "sha256": "c73d272d59d71522d3d61eea66cf61da205c739743d720d15cb70f78bd5e07ac" + }, + { + "path": "src/shared/agent-session-journal-item-key.ts", + "sha256": "09ebe4758e3f38b5b7591f07f7b4dbfd04c7cc01713c9b0afe03b07b656d1fe8" + }, + { + "path": "src/shared/agent-session-journal-types.ts", + "sha256": "4f85b301aa12ea14ac87b0099135247ad0758529cdb2ce003bb6a1031cf279ea" + }, + { + "path": "src/shared/agent-session-journal-schemas.ts", + "sha256": "579ab7671b7f8e46374f11b6ac669bf659bd5505345c49d7dd1c9acc4b0ef564" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-row-schema.ts", + "sha256": "de9c4f6324feef96fd33cfb0698f4c34d7e02694809fb133757146ead0cbb9c3" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.ts", + "sha256": "9361c2255ac501b4c18baf22dd4e74936bc8c1181f227329b97bf1af7593bf4c" + }, + { + "path": "src/main/codex/codex-structured-journal-sink.ts", + "sha256": "e05226f2cb906557a0811de780b9f30e4bbedbd6253cd4948e5518646b63cf26" + }, + { + "path": "src/main/codex/codex-structured-journal-compactions.ts", + "sha256": "45c8e450be19cf8b88b5ffa90311e80390f40a32fd2ceaddf6bfd4cf7992ba03" + }, + { + "path": "src/main/codex/codex-goal-journal-identity.ts", + "sha256": "55d5c3a3120ea038aac9ae64cd51c5bfdf0e13af1306dca01009993cf482ad08" + }, + { + "path": "src/main/codex/codex-structured-journal-goals.ts", + "sha256": "d14794482031e86cf21f2c859ce8097f0759a5a0b2bc8f97ea26598ecfd190a5" + }, + { + "path": "src/shared/agent-turn-lifecycle-text.ts", + "sha256": "bf3a91d9c5157be19c9c88aed96e2c4e076e744362b34ddf2b466c707067e32a" + }, + { + "path": "src/shared/agent-session-turn-record.ts", + "sha256": "1a3df88a627b4744ccbe8168860033dfef66bd4ff028c6431af6a6ac2f63194f" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-terminal-settlement.ts", + "sha256": "9344b1593bf91a49a6e1d2fb9604e2890939eb1b1f0cadbbdae7194db57a8080" + }, + { + "path": "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts", + "sha256": "48828e4ee21858075cbb87ec1caa4a82991a55f80928482615d1df7ec5dc0fb2" + }, + { + "path": "src/main/codex/codex-command-lifecycle.ts", + "sha256": "7b480d3b111304e3caff71768fb3b11ab6c36e1fa8f5b0440623a77cac2382c2" + }, + { + "path": "src/main/codex/codex-structured-item-stream-bounds.ts", + "sha256": "88e9efa8a9c163905f1a5eb5eaf316fba2f194d76c5a94ed7f55636cc0d74ae0" + }, + { + "path": "src/main/codex/codex-item-stream-retention.ts", + "sha256": "66d1f1d51ed6711e20f81698b14be2d003e899dbc587ac1306b6a656cf951b96" + }, + { + "path": "src/main/codex/codex-structured-item-stream-events.ts", + "sha256": "da2a9da9025af354ce705a190badadf822f9ac3b403eef04aa0ef8aaf6f023de" + }, + { + "path": "src/main/codex/codex-structured-item-streams.ts", + "sha256": "b70c3306e8bec3587342afc3bc156f40903513fa7ff3865b217ba4cc912cd89f" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-values.ts", + "sha256": "fb66d47e32f4040f1b248a886b077506d23d0492264412e6cc3c96b579de425d" + }, + { + "path": "src/main/codex/codex-structured-dispatch-echo.ts", + "sha256": "4be5e3b55bd0a5507878798f221e758444dc82ede2f14e741f08b6b85979cd7a" + }, + { + "path": "src/main/codex/codex-structured-journal-items.ts", + "sha256": "7a9dc981cdcb1931d620feaa5523b486600c50c241f223ffdc71aa9edeb4a5bb" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts", + "sha256": "e4382b9d5b8a9cdba94b2ee71188119790987b11fd0f8f9718ff4be899e9a9fa" + }, + { + "path": "src/main/codex/codex-structured-prompt-items.ts", + "sha256": "813e8fdf2768663e31822699a5a7311df8e714ca2a269dcbb1f42e8d196ac456" + }, + { + "path": "src/main/codex/codex-structured-journal-prompts.ts", + "sha256": "806f61f913c28593b02b6ea6dcb68559b126bf939b91b1e5cf4f43fedabf45a2" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turns.ts", + "sha256": "9042b2393d7d8898ed4f7436669dfbc82504e15b9ea90eb00e1518ee4db254e0" + }, + { + "path": "src/main/codex/codex-structured-journal-settlement.ts", + "sha256": "761616c292e15894b337fdd171acd7c2c1272fcabdbad3ea460f8831a923c797" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-frames.ts", + "sha256": "0679aea6b6fc274448d007c292218f90bfdd8ad3ed4ce4a7d59d0a15aae9e455" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-restore.ts", + "sha256": "a2e521b7cae677a5b3fd671065e5e4b1c812e2444f272df0f79feddae3640652" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turn-boundaries.ts", + "sha256": "bb482b2c49fa3754b76e1e749185bb2804409e017f909c0453022ead07f99a03" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turn-state.ts", + "sha256": "f6dd637f0e77f193923c3f3843b86d12786b75b9ce76e5a7e057a7187a832fe5" + }, + { + "path": "src/main/codex/codex-structured-journal-translation.ts", + "sha256": "56e17b9649471e804d29d1d54a3e14b4298ca8af2e25c4759dc37fce000b06a1" + }, + { + "path": "src/main/codex/codex-server-request-disposition.ts", + "sha256": "ddd362e00a38ff0166d35bc4cca51e4f79e377f188a9e07f5966da7c056854a0" + }, + { + "path": "src/shared/node-bounded-json-stringify.ts", + "sha256": "fcadabc537aa3125d1ee514026ecca5b8e2a76aabca7e29f08d3d35008001622" + }, + { + "path": "src/shared/remote-runtime-client-error.ts", + "sha256": "593ff4ee6466c4b18b69f65a20631967205fb8e1bd5e46239cf7f526b764156a" + }, + { + "path": "src/shared/remote-runtime-memory-limits.ts", + "sha256": "862a317d7f564a2cc54cfa60b6c093de5dc8a61714eac45daa178608c1f6acb1" + }, + { + "path": "src/main/native-chat/agent-session-wire/agent-session-history-page-bounds.ts", + "sha256": "57e49a0ea59cdccf4a4fc1532d43d2c127b077233602139b2d36d79fd547fc9d" + }, + { + "path": "src/main/codex/codex-structured-rewind.ts", + "sha256": "710115f53bc673f7d7259ec9e1d6b02e7827c348225c1e6a2006ed5eaf06e6bc" + }, + { + "path": "src/main/codex/codex-structured-provider-events.ts", + "sha256": "a411dc378e521ea8fc915e2a2a21476341a30f440e8233ae5649205d2a1e2e44" + } + ] + } + } +} diff --git a/docs/audits/codex-prompt-claim-retention/fix.patch b/docs/audits/codex-prompt-claim-retention/fix.patch new file mode 100644 index 00000000000..9c424fe1b10 --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/fix.patch @@ -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) + ) + ) diff --git a/docs/audits/codex-prompt-claim-retention/node-results.json b/docs/audits/codex-prompt-claim-retention/node-results.json new file mode 100644 index 00000000000..60b11e1be5a --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/node-results.json @@ -0,0 +1,1238 @@ +{ + "capturedAt": "2026-09-17T02:39:39.462Z", + "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" + }, + "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": { + "src/main/codex/codex-prompt-registry.ts": { + "before": "254c530216f47abfc3572bb1689161ad3c10775af2cdceda7e94f443e52c0691", + "after": "3d33f98215b9547d661c0a4f6aa8b9a8a6a88fa953d90652cb3fb9953425a539" + } + }, + "countsOnly": true, + "noPayloadAmplification": true, + "results": { + "original": { + "ordinaryAfterCompletion": 0, + "cancelledPrompts": 32, + "sizesAfterEviction": { + "prompts": 128, + "journalBindings": 256 + }, + "afterEvictionBeforeCompletion": 32, + "afterExactTurnCompletion": 32, + "afterAllLookupMapsEmpty": 32, + "afterSessionClear": 0, + "oldAfterReplacementCompletion": 1, + "replacementClaimPreserved": true, + "wrongThreadPreserved": true, + "wrongTurnPreserved": true, + "rejectedCompletionPreserved": true, + "successfulInterruptRequests": 34 + }, + "candidate": { + "ordinaryAfterCompletion": 0, + "cancelledPrompts": 32, + "sizesAfterEviction": { + "prompts": 128, + "journalBindings": 256 + }, + "afterEvictionBeforeCompletion": 32, + "afterExactTurnCompletion": 0, + "afterAllLookupMapsEmpty": 0, + "afterSessionClear": 0, + "oldAfterReplacementCompletion": 0, + "replacementClaimPreserved": true, + "wrongThreadPreserved": true, + "wrongTurnPreserved": true, + "rejectedCompletionPreserved": true, + "successfulInterruptRequests": 34 + } + }, + "versions": { + "original": { + "bundleSha256": "e060beb8d88044d6abe07134880d9ddcbb8487464c963d0cce336454e789bcc8", + "dependencies": [ + { + "path": "src/main/native-chat/agent-session-journal/journal-payload-bounds.ts", + "sha256": "a421da6c6eee9346746a69f8ba7ff966ce81969dfa0a93f58784cac27cf8cdad" + }, + { + "path": "src/shared/agent-session-wire-refusals.ts", + "sha256": "25123cce70418deb3d2f75cf5c3b7431aa6a8c3b834f022cd697230345c595ea" + }, + { + "path": "src/shared/agent-session-background-task-wire.ts", + "sha256": "6a756544bb07864d5a6e4573992f4b3bb0d0788acb686c16da177095be23167c" + }, + { + "path": "src/shared/agent-session-wire.ts", + "sha256": "466b641465f92957dafe873c68d4cb42f50aeb0768a111397050b50aa96ac64f" + }, + { + "path": "src/main/codex/codex-prompt-registry-bounds.ts", + "sha256": "f6bfd21bcc2ccf7005a93a4764d17235887f11380b0b29301410b0f291a5c618" + }, + { + "path": "src/main/codex/codex-item-field-readers.ts", + "sha256": "aa4b178c562a63995b7de63c6f5daaf7b6b610fb163fb0a357cc230314076323" + }, + { + "path": "src/main/codex/codex-prompt-registry.ts", + "sha256": "254c530216f47abfc3572bb1689161ad3c10775af2cdceda7e94f443e52c0691" + }, + { + "path": "src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts", + "sha256": "9a7bb24e419dbd41f4aaaa20b87dd82859579d663ebb800fc605df4b471749ef" + }, + { + "path": "src/main/codex/codex-structured-prompt-replies.ts", + "sha256": "7b927e9910031ee80ab885ce1d713133d3ee614c5b6d460a5c91ff3768d9cb2e" + }, + { + "path": "src/shared/child-process/cancel-process-acquisition.ts", + "sha256": "709a04316da5f3528e33e02bbb6f054012714efc73628a04fd08289eae0c42d5" + }, + { + "path": "src/main/codex/codex-structured-acquisition-window.ts", + "sha256": "6828970d450a53fcb30ecc0ecb20d8452395bf579cc2295ee53415c299d039a4" + }, + { + "path": "src/main/codex/codex-structured-session-state.ts", + "sha256": "271837e87efac55238342a26ccdf350d9eb663b1d38a2553886429d1376ab1f4" + }, + { + "path": "src/main/codex/codex-structured-prompt-ownership.ts", + "sha256": "8d86f3784b79a1470e4002527bdfa505efe2762cba865de7716442f7d9730013" + }, + { + "path": "src/shared/child-process/windows-command-line.ts", + "sha256": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + }, + { + "path": "src/shared/child-process/windows-cmd-shim-resolution.ts", + "sha256": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + }, + { + "path": "src/shared/child-process/spawn-resolution.ts", + "sha256": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + }, + { + "path": "src/shared/child-process/process-tree-kill-gate.ts", + "sha256": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + }, + { + "path": "src/shared/child-process/process-tree-termination.ts", + "sha256": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + }, + { + "path": "src/shared/child-process/bounded-output-sink.ts", + "sha256": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + }, + { + "path": "src/shared/child-process/child-termination-reporter.ts", + "sha256": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + }, + { + "path": "src/shared/child-process/process-spec.ts", + "sha256": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + }, + { + "path": "src/shared/child-process/run-process.ts", + "sha256": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + }, + { + "path": "src/shared/child-process/retryable-process-exit-proof.ts", + "sha256": "411a39e4508763d8c4ef331bd16e88c1fe497f03188493241afcce6eab0a6e65" + }, + { + "path": "src/main/codex/codex-app-server-posix-supervisor.ts", + "sha256": "ccd4ff7a43d3ba92b9becab45304d5f5522f5c1d5aa53d379fdd47845d0fe545" + }, + { + "path": "src/main/codex/codex-app-server-capability-signal.ts", + "sha256": "43cf352901aeff4ed9143f51f224770f3dc92bf79dcfac6875bbc64105184c2f" + }, + { + "path": "src/main/codex/codex-process-exit-deadline.ts", + "sha256": "952cbb7c8778b8187ff443f8c045be53c14c3778af1b2ec77a3af5cd3ff13364" + }, + { + "path": "src/shared/system-cli-install-dirs.ts", + "sha256": "635b59f51022c9f31bf7720385b343018d4ce849220d422115e74f5351a9f21d" + }, + { + "path": "src/shared/node-cli-command-resolution.ts", + "sha256": "5eecee0c0d6296b962315c2ef2ae228a1bb1b6a7449f46d5afe3977f0af1e145" + }, + { + "path": "src/main/codex/codex-app-server-process-tree-kill.ts", + "sha256": "0a370b1d40509aa967634d6551f6baf5462b648910ddb79c2624555a84116b0b" + }, + { + "path": "src/shared/main-process-ndjson-framer.ts", + "sha256": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490" + }, + { + "path": "src/main/codex/codex-app-server-record-reader.ts", + "sha256": "b4d600293228ae2429090facc3db6f98dff449f2c3d82f35a0bea11fe4d70048" + }, + { + "path": "src/main/codex/codex-app-server-session.ts", + "sha256": "9adfbab8e229d3998d973ad9abfe647015ff21a5517fa6e5c7b32acf0a446eb8" + }, + { + "path": "src/main/codex/codex-app-server-exit-error.ts", + "sha256": "d58cdd97ae3ad4a786c7c4a0b1062ee3c5bb15b0943046dda7fff4b51ede21d1" + }, + { + "path": "src/main/codex/codex-app-server-handshake.ts", + "sha256": "3dc86d5961076713cb67ff58f593ca0fe848cd2f6b5a3a0a5da83773f49e6516" + }, + { + "path": "src/main/codex/codex-app-server-handshake-exit-proof.ts", + "sha256": "01832f6ab97bb68566221151d6a88e03cd5461f699991ad0e2c1f52573541ca1" + }, + { + "path": "src/shared/crash-reporting-diagnostic-bundle.ts", + "sha256": "2ac76a8941d4e8ae8d4e0a95ac9e10102602670e4c8fdbcb88a03b014903d1ba" + }, + { + "path": "src/shared/crash-report-signature-lines.ts", + "sha256": "564936f235f0c9faefc0c9f18f2fcbc588e5bef69ecd1071baac5a30a7c662a8" + }, + { + "path": "src/shared/posix-wait-status.ts", + "sha256": "1eb72525881baa2815548d31d2b83aaecf88e5dfd0258dbcaa5cda1e8fdcd69f" + }, + { + "path": "src/shared/crash-report-exit-code.ts", + "sha256": "a73f4e6fce72f11f5eec32c2a22d2e749f330f434aef20cec6dc03dbde55ebc6" + }, + { + "path": "src/shared/react-update-depth-attribution.ts", + "sha256": "15fd538fa4953088b63d9307fc575d4f795614b445a558340d80d61bb4c50929" + }, + { + "path": "src/shared/crash-report-attribution-lines.ts", + "sha256": "3db6cabca0444343347a90e6ec71a015a79489097e2484202cf2b57232483a6e" + }, + { + "path": "src/shared/crash-report-redaction.ts", + "sha256": "afd00bd49c77398425ee481070818e98067e90bb472a7454c571bf77ceee9c7f" + }, + { + "path": "src/shared/crash-reporting.ts", + "sha256": "e9749318038d145b7ae1819ffed6b285815c502d0eba13181c48b73b1bd3e918" + }, + { + "path": "src/main/observability/redactor.ts", + "sha256": "59190309dbef22508e3f2ef27b0190093d5aaab8cce146d6d49a89eb9d0a2b73" + }, + { + "path": "src/main/observability/tracer.ts", + "sha256": "82b6f4ec0be07b6aa612038b21b9f67b0ef199f694d29dc9f7b54b55dba9776e" + }, + { + "path": "src/main/crash-reporting/crash-breadcrumb-store.ts", + "sha256": "8848be8a897e0d1a214dbbaf87a0377d493ee83358763c0fc286e693889b2cb9" + }, + { + "path": "src/main/crash-reporting/main-process-lifecycle-identity.ts", + "sha256": "797cd37e5af6ec71f91636bc779274572a62ea67e5fa2874bb1f2f771020c950" + }, + { + "path": "src/main/crash-reporting/durable-crash-breadcrumb.ts", + "sha256": "477e28157e119f3bd85bc77d4adb185d36e44103b8a087d680715ce71b05dea7" + }, + { + "path": "src/main/crash-reporting/self-initiated-tree-kill-log.ts", + "sha256": "eb28b4a513a3cbc30586df30569adf29d828f059522ebe6c751d55b233c13987" + }, + { + "path": "src/shared/app-environment.ts", + "sha256": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140" + }, + { + "path": "src/main/orca-chromium-process-pids.ts", + "sha256": "960a15f005ff997fa379972a8c2abe3410b78c6facee449e8a5350754cfce965" + }, + { + "path": "src/main/own-chromium-tree-kill-guard.ts", + "sha256": "3a59cccd8cf923506aacad58f1c5d11b49f47a21e6c6dee8559912ee3c5cefb5" + }, + { + "path": "src/main/windows-process-tree-kill.ts", + "sha256": "83236378e93e3a15189ca02febf6b9b94d9d7eadafb46f0fcb72051904e6f45d" + }, + { + "path": "src/shared/process-table-index.ts", + "sha256": "30f236b7d0275f05673c0a7982bb0aa67c9319762749f5a583c5c31e51e31f36" + }, + { + "path": "src/shared/process-table-snapshot.ts", + "sha256": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + }, + { + "path": "src/shared/process-table-snapshot-reader.ts", + "sha256": "7d35601a2a7bf7a9c5b4a897995e0e8a15775b4d35939f3dd1abe1a042bb1c5e" + }, + { + "path": "src/main/windows/windows-command-line-recovery-health.ts", + "sha256": "a135da3611d54fecb0dba52b4d81e06ca1532680424b55e7efa315b7c886ec33" + }, + { + "path": "src/shared/child-process/windows-system-binary.ts", + "sha256": "0c42c7c454534a0937f4153e9708aeb243bd930d130ffc6493ac42d21405606e" + }, + { + "path": "src/main/windows/windows-process-table-cim-scan.ts", + "sha256": "ecfa487a3139cc087eae78ff61bf358f7eb62aec3b20e9d064d96b6e37862ab0" + }, + { + "path": "src/main/windows/windows-process-table.ts", + "sha256": "de0565f724f49ca393bb55c1618e697ea34e98bfee132c196d0c3e97713aabe1" + }, + { + "path": "src/main/providers/windows-foreground-process-rows.ts", + "sha256": "374accddf905f5b60fb1be4010e8160bfaf7f1a3834ecaffbf0119e555c7a765" + }, + { + "path": "src/main/windows-pty-root-identity.ts", + "sha256": "5e1ec017199f1a78150064d22eca4fd724874473c042f78fa236f3831f761711" + }, + { + "path": "src/main/pty-descendant-termination.ts", + "sha256": "dbdfe91d9248b4fbfbdac1733237ff3a4776e596a6a127fbdf5c4fb1d987b61e" + }, + { + "path": "src/main/pty-descendant-exit-verification.ts", + "sha256": "1f7a14a8273a228180538b3d37a20e0631e60a2e497134de499907eabfeb9b77" + }, + { + "path": "src/main/runtime/agent-session-process-identity-probe.ts", + "sha256": "4f6bc0fc286f74b07a26bdfbe69a9124498deb289c5d96db8b1334e12fdcce33" + }, + { + "path": "src/main/codex/codex-structured-owner-identity.ts", + "sha256": "b5b286f638515cae36539d0061c3808e64e6b2ed464f9326255fcd2f655d85f7" + }, + { + "path": "src/main/runtime/agent-session-spawn-token-readback.ts", + "sha256": "1b2481a5487b9ee7e21a2103b468e69d91879f84a2a330b7fa81c7a3b056e399" + }, + { + "path": "src/main/runtime/agent-session-spawn-token-process-scan.ts", + "sha256": "57fe5ac196d53e266b4d5e9ea0fab7311a292b5f281cbe56eea98c5579634bfc" + }, + { + "path": "src/main/codex/codex-app-server-process-teardown.ts", + "sha256": "183110df0c529c8eaec77ed457a9b17c5322d313482119f488f019dbf6d73f75" + }, + { + "path": "src/main/codex/codex-app-server-frame-size-error.ts", + "sha256": "8f567def6e41edb29c3a56b2be4377f64a99999c76bf24be2980a296ce6c01e5" + }, + { + "path": "src/main/codex/codex-app-server-jsonl.ts", + "sha256": "428b802a6704ec57efb26c696ad9e77da727a990b881665fdf5f68542cf143b4" + }, + { + "path": "src/main/codex/codex-app-server-request-error.ts", + "sha256": "67140be11e2b29f93d8a352552af733d228ac1808b6cf53bb7227e3906794eea" + }, + { + "path": "src/main/codex/codex-app-server-record-prefix.ts", + "sha256": "e70f8492995568e2891af0f58a78e5eb9a2f5f1970b9a561aa1bd7af32673f0d" + }, + { + "path": "src/main/codex/codex-app-server-record-dispatch.ts", + "sha256": "48554c6f9d4cb056d1a8e2e495b1c3fdadecf891d8f02d9599a7af3989f6ab23" + }, + { + "path": "src/main/codex/codex-app-server-connection.ts", + "sha256": "30fcca9f4cda5d6cceb9215905d7f3ead2361764b80e0e67ba0bcbb55e00672d" + }, + { + "path": "src/main/codex/codex-structured-thread-facts.ts", + "sha256": "a244c847c5ca22c3e3fac3b075b51f0a566f8cf6726731f69f50f7eb079b063a" + }, + { + "path": "src/main/codex/codex-structured-turn-processes.ts", + "sha256": "6a942ed29995d8001f8c041c693ed7bd30ecfc3fac5dd30e4c5ec70c0a12396a" + }, + { + "path": "src/main/codex/codex-structured-turn-cancellation.ts", + "sha256": "518b8e5a2f6dc4f8e46e01366c0a61830d6c805b68101484726bd62bbfc831a8" + }, + { + "path": "src/shared/orca-dispatch-status-prompt.ts", + "sha256": "fd9fedb3c839cd9d17b545329937c8fb6b9fa081120eb4678a9fb66f4252629e" + }, + { + "path": "src/shared/agent-status-field-normalization.ts", + "sha256": "a60c119eade7026c9e6f37ff98e77e718bd0cb41e929b0e256872981411e3328" + }, + { + "path": "src/main/native-chat/agent-session-wire/provider-frame-activity.ts", + "sha256": "9888661f598cee4d622f22b208f9ba1fc6615df6424328cf818cebe9fe86b8d7" + }, + { + "path": "src/main/codex/codex-subagent-activity.ts", + "sha256": "5cdf18a4ea2c68178a67a845d4c95672a0d94bb282e8e15c3a930c6dd59151e4" + }, + { + "path": "src/shared/native-chat-types.ts", + "sha256": "ca9440aba61822a8b92cc1009790d5f6e7bdb2f570cf159349a42027dfd40def" + }, + { + "path": "src/shared/native-chat-subagent-summary.ts", + "sha256": "a2cdf19a07edf3b50a971af22044608d50ea9cfebd07abc01af38755a2143077" + }, + { + "path": "src/main/codex/codex-subagent-executions.ts", + "sha256": "2a356e9a108a302e1131d40112401a48c582b2d5560eab073004a7efefde5259" + }, + { + "path": "src/main/codex/codex-subagent-group-body.ts", + "sha256": "9ff41e055ee33fb556f2079604271f13e696ca0114eaaa9a5f25166e5fa49280" + }, + { + "path": "src/main/codex/codex-structured-journal-limits.ts", + "sha256": "2fd2d5015b73fb948df6abdb6469039b1ee3a680db5119b1a44ebf82bc152492" + }, + { + "path": "src/main/codex/codex-subagent-roster.ts", + "sha256": "bafd291665fb178b0912bd2a2723910c00890f0766aae7f70f8bed5d417542b1" + }, + { + "path": "src/shared/native-chat-turn-status.ts", + "sha256": "4ce2b64fa7818106814c135e5daf77bb183f601c35f9f08e5b7ff847fba01378" + }, + { + "path": "src/shared/native-chat-tool-identity.ts", + "sha256": "8c98eec1a53b26a67ed48a0859c5863f5accbe67e013c4eb5a0f8b32441ee925" + }, + { + "path": "src/main/codex/codex-goal-journal-rows.ts", + "sha256": "415846ca105300349778da7637216ca6ce978c7036219f6b3038f7228ba8724e" + }, + { + "path": "src/main/native-chat/agent-session-wire/provider-frame-disposition.ts", + "sha256": "f24008cebb1b8a77b0cbd091e49140a4bf99c676bc61eedeb0a82f1263244a7c" + }, + { + "path": "src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts", + "sha256": "f8169a024a573adfc97e4eb8c99ecc4e80f82e9f94a04a29011a6b8051cc7626" + }, + { + "path": "src/shared/raster-image-dimensions.ts", + "sha256": "0d7462b8b2e53bfcdcbd32803edcfb85ed0063f56d45cf230fb59aac4783d063" + }, + { + "path": "src/shared/raster-image-preview-limits.ts", + "sha256": "f2d836b354b3951912f1ec689d84b82aa1aade89ed8e9c6f76be2321d600e9a7" + }, + { + "path": "src/shared/raster-image-base64-preview.ts", + "sha256": "89baf40f4637615456888e0f35115517a8445a3b1cb640b7bc495dd9a98fcedb" + }, + { + "path": "src/shared/image-data-uri.ts", + "sha256": "3eb9d2bda499b8f10c74783f9de766041469b140a9b96b2b5be7d97528688ce0" + }, + { + "path": "src/main/codex/codex-image-item-translation.ts", + "sha256": "c4f88236c707472cbcfcdfb1a04e51c738f8741aa5847a33f730759499522f5e" + }, + { + "path": "src/main/codex/codex-command-action-class.ts", + "sha256": "93b7bceb66a9e5bfae680e018b61bddb5c1117fe26243088ceccc8738841f966" + }, + { + "path": "src/main/codex/codex-thread-item-identity.ts", + "sha256": "04b98b367a4d658fb759ebc4fc73ea4ae731ae3f238f191c8ce002b2a2c9e2a8" + }, + { + "path": "src/main/codex/codex-turn-ordinals.ts", + "sha256": "9b7cf66986235bcacd6c198950452c7f40bd559a3a0ce20995aac496a1e32d7f" + }, + { + "path": "src/main/codex/codex-structured-item-translation.ts", + "sha256": "8ab9debb279baf3a5043ffe754c8911755c6e4ca9edfc0a094671c4d5d166639" + }, + { + "path": "src/main/codex/codex-structured-journal-contracts.ts", + "sha256": "59dbe0f4f38aea3ff31ae62d7a8d244636e2813bb4a0f1e1c2db3362bdf193ac" + }, + { + "path": "src/main/codex/codex-structured-journal-generic-frames.ts", + "sha256": "5fe2414993c44075d9c979d76091c89611360cb166f650e4483d1e6bad3b30d2" + }, + { + "path": "src/main/native-chat/agent-session-wire/structured-session-compaction.ts", + "sha256": "c73d272d59d71522d3d61eea66cf61da205c739743d720d15cb70f78bd5e07ac" + }, + { + "path": "src/shared/agent-session-journal-item-key.ts", + "sha256": "09ebe4758e3f38b5b7591f07f7b4dbfd04c7cc01713c9b0afe03b07b656d1fe8" + }, + { + "path": "src/shared/agent-session-journal-types.ts", + "sha256": "4f85b301aa12ea14ac87b0099135247ad0758529cdb2ce003bb6a1031cf279ea" + }, + { + "path": "src/shared/agent-session-journal-schemas.ts", + "sha256": "579ab7671b7f8e46374f11b6ac669bf659bd5505345c49d7dd1c9acc4b0ef564" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-row-schema.ts", + "sha256": "de9c4f6324feef96fd33cfb0698f4c34d7e02694809fb133757146ead0cbb9c3" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.ts", + "sha256": "9361c2255ac501b4c18baf22dd4e74936bc8c1181f227329b97bf1af7593bf4c" + }, + { + "path": "src/main/codex/codex-structured-journal-sink.ts", + "sha256": "e05226f2cb906557a0811de780b9f30e4bbedbd6253cd4948e5518646b63cf26" + }, + { + "path": "src/main/codex/codex-structured-journal-compactions.ts", + "sha256": "45c8e450be19cf8b88b5ffa90311e80390f40a32fd2ceaddf6bfd4cf7992ba03" + }, + { + "path": "src/main/codex/codex-goal-journal-identity.ts", + "sha256": "55d5c3a3120ea038aac9ae64cd51c5bfdf0e13af1306dca01009993cf482ad08" + }, + { + "path": "src/main/codex/codex-structured-journal-goals.ts", + "sha256": "d14794482031e86cf21f2c859ce8097f0759a5a0b2bc8f97ea26598ecfd190a5" + }, + { + "path": "src/shared/agent-turn-lifecycle-text.ts", + "sha256": "bf3a91d9c5157be19c9c88aed96e2c4e076e744362b34ddf2b466c707067e32a" + }, + { + "path": "src/shared/agent-session-turn-record.ts", + "sha256": "1a3df88a627b4744ccbe8168860033dfef66bd4ff028c6431af6a6ac2f63194f" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-terminal-settlement.ts", + "sha256": "9344b1593bf91a49a6e1d2fb9604e2890939eb1b1f0cadbbdae7194db57a8080" + }, + { + "path": "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts", + "sha256": "48828e4ee21858075cbb87ec1caa4a82991a55f80928482615d1df7ec5dc0fb2" + }, + { + "path": "src/main/codex/codex-command-lifecycle.ts", + "sha256": "7b480d3b111304e3caff71768fb3b11ab6c36e1fa8f5b0440623a77cac2382c2" + }, + { + "path": "src/main/codex/codex-structured-item-stream-bounds.ts", + "sha256": "88e9efa8a9c163905f1a5eb5eaf316fba2f194d76c5a94ed7f55636cc0d74ae0" + }, + { + "path": "src/main/codex/codex-item-stream-retention.ts", + "sha256": "66d1f1d51ed6711e20f81698b14be2d003e899dbc587ac1306b6a656cf951b96" + }, + { + "path": "src/main/codex/codex-structured-item-stream-events.ts", + "sha256": "da2a9da9025af354ce705a190badadf822f9ac3b403eef04aa0ef8aaf6f023de" + }, + { + "path": "src/main/codex/codex-structured-item-streams.ts", + "sha256": "b70c3306e8bec3587342afc3bc156f40903513fa7ff3865b217ba4cc912cd89f" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-values.ts", + "sha256": "fb66d47e32f4040f1b248a886b077506d23d0492264412e6cc3c96b579de425d" + }, + { + "path": "src/main/codex/codex-structured-dispatch-echo.ts", + "sha256": "4be5e3b55bd0a5507878798f221e758444dc82ede2f14e741f08b6b85979cd7a" + }, + { + "path": "src/main/codex/codex-structured-journal-items.ts", + "sha256": "7a9dc981cdcb1931d620feaa5523b486600c50c241f223ffdc71aa9edeb4a5bb" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts", + "sha256": "e4382b9d5b8a9cdba94b2ee71188119790987b11fd0f8f9718ff4be899e9a9fa" + }, + { + "path": "src/main/codex/codex-structured-prompt-items.ts", + "sha256": "813e8fdf2768663e31822699a5a7311df8e714ca2a269dcbb1f42e8d196ac456" + }, + { + "path": "src/main/codex/codex-structured-journal-prompts.ts", + "sha256": "806f61f913c28593b02b6ea6dcb68559b126bf939b91b1e5cf4f43fedabf45a2" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turns.ts", + "sha256": "9042b2393d7d8898ed4f7436669dfbc82504e15b9ea90eb00e1518ee4db254e0" + }, + { + "path": "src/main/codex/codex-structured-journal-settlement.ts", + "sha256": "761616c292e15894b337fdd171acd7c2c1272fcabdbad3ea460f8831a923c797" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-frames.ts", + "sha256": "0679aea6b6fc274448d007c292218f90bfdd8ad3ed4ce4a7d59d0a15aae9e455" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-restore.ts", + "sha256": "a2e521b7cae677a5b3fd671065e5e4b1c812e2444f272df0f79feddae3640652" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turn-boundaries.ts", + "sha256": "bb482b2c49fa3754b76e1e749185bb2804409e017f909c0453022ead07f99a03" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turn-state.ts", + "sha256": "f6dd637f0e77f193923c3f3843b86d12786b75b9ce76e5a7e057a7187a832fe5" + }, + { + "path": "src/main/codex/codex-structured-journal-translation.ts", + "sha256": "56e17b9649471e804d29d1d54a3e14b4298ca8af2e25c4759dc37fce000b06a1" + }, + { + "path": "src/main/codex/codex-server-request-disposition.ts", + "sha256": "ddd362e00a38ff0166d35bc4cca51e4f79e377f188a9e07f5966da7c056854a0" + }, + { + "path": "src/shared/node-bounded-json-stringify.ts", + "sha256": "fcadabc537aa3125d1ee514026ecca5b8e2a76aabca7e29f08d3d35008001622" + }, + { + "path": "src/shared/remote-runtime-client-error.ts", + "sha256": "593ff4ee6466c4b18b69f65a20631967205fb8e1bd5e46239cf7f526b764156a" + }, + { + "path": "src/shared/remote-runtime-memory-limits.ts", + "sha256": "862a317d7f564a2cc54cfa60b6c093de5dc8a61714eac45daa178608c1f6acb1" + }, + { + "path": "src/main/native-chat/agent-session-wire/agent-session-history-page-bounds.ts", + "sha256": "57e49a0ea59cdccf4a4fc1532d43d2c127b077233602139b2d36d79fd547fc9d" + }, + { + "path": "src/main/codex/codex-structured-rewind.ts", + "sha256": "710115f53bc673f7d7259ec9e1d6b02e7827c348225c1e6a2006ed5eaf06e6bc" + }, + { + "path": "src/main/codex/codex-structured-provider-events.ts", + "sha256": "a411dc378e521ea8fc915e2a2a21476341a30f440e8233ae5649205d2a1e2e44" + } + ] + }, + "candidate": { + "bundleSha256": "6b7ec91709cad63ae089187e297914243757d59991842be0c0c2eb92514b3548", + "dependencies": [ + { + "path": "src/main/native-chat/agent-session-journal/journal-payload-bounds.ts", + "sha256": "a421da6c6eee9346746a69f8ba7ff966ce81969dfa0a93f58784cac27cf8cdad" + }, + { + "path": "src/shared/agent-session-wire-refusals.ts", + "sha256": "25123cce70418deb3d2f75cf5c3b7431aa6a8c3b834f022cd697230345c595ea" + }, + { + "path": "src/shared/agent-session-background-task-wire.ts", + "sha256": "6a756544bb07864d5a6e4573992f4b3bb0d0788acb686c16da177095be23167c" + }, + { + "path": "src/shared/agent-session-wire.ts", + "sha256": "466b641465f92957dafe873c68d4cb42f50aeb0768a111397050b50aa96ac64f" + }, + { + "path": "src/main/codex/codex-prompt-registry-bounds.ts", + "sha256": "f6bfd21bcc2ccf7005a93a4764d17235887f11380b0b29301410b0f291a5c618" + }, + { + "path": "src/main/codex/codex-item-field-readers.ts", + "sha256": "aa4b178c562a63995b7de63c6f5daaf7b6b610fb163fb0a357cc230314076323" + }, + { + "path": "src/main/codex/codex-prompt-registry.ts", + "sha256": "3d33f98215b9547d661c0a4f6aa8b9a8a6a88fa953d90652cb3fb9953425a539" + }, + { + "path": "src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts", + "sha256": "9a7bb24e419dbd41f4aaaa20b87dd82859579d663ebb800fc605df4b471749ef" + }, + { + "path": "src/main/codex/codex-structured-prompt-replies.ts", + "sha256": "7b927e9910031ee80ab885ce1d713133d3ee614c5b6d460a5c91ff3768d9cb2e" + }, + { + "path": "src/shared/child-process/cancel-process-acquisition.ts", + "sha256": "709a04316da5f3528e33e02bbb6f054012714efc73628a04fd08289eae0c42d5" + }, + { + "path": "src/main/codex/codex-structured-acquisition-window.ts", + "sha256": "6828970d450a53fcb30ecc0ecb20d8452395bf579cc2295ee53415c299d039a4" + }, + { + "path": "src/main/codex/codex-structured-session-state.ts", + "sha256": "271837e87efac55238342a26ccdf350d9eb663b1d38a2553886429d1376ab1f4" + }, + { + "path": "src/main/codex/codex-structured-prompt-ownership.ts", + "sha256": "8d86f3784b79a1470e4002527bdfa505efe2762cba865de7716442f7d9730013" + }, + { + "path": "src/shared/child-process/windows-command-line.ts", + "sha256": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + }, + { + "path": "src/shared/child-process/windows-cmd-shim-resolution.ts", + "sha256": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + }, + { + "path": "src/shared/child-process/spawn-resolution.ts", + "sha256": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + }, + { + "path": "src/shared/child-process/process-tree-kill-gate.ts", + "sha256": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + }, + { + "path": "src/shared/child-process/process-tree-termination.ts", + "sha256": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + }, + { + "path": "src/shared/child-process/bounded-output-sink.ts", + "sha256": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + }, + { + "path": "src/shared/child-process/child-termination-reporter.ts", + "sha256": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + }, + { + "path": "src/shared/child-process/process-spec.ts", + "sha256": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + }, + { + "path": "src/shared/child-process/run-process.ts", + "sha256": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + }, + { + "path": "src/shared/child-process/retryable-process-exit-proof.ts", + "sha256": "411a39e4508763d8c4ef331bd16e88c1fe497f03188493241afcce6eab0a6e65" + }, + { + "path": "src/main/codex/codex-app-server-posix-supervisor.ts", + "sha256": "ccd4ff7a43d3ba92b9becab45304d5f5522f5c1d5aa53d379fdd47845d0fe545" + }, + { + "path": "src/main/codex/codex-app-server-capability-signal.ts", + "sha256": "43cf352901aeff4ed9143f51f224770f3dc92bf79dcfac6875bbc64105184c2f" + }, + { + "path": "src/main/codex/codex-process-exit-deadline.ts", + "sha256": "952cbb7c8778b8187ff443f8c045be53c14c3778af1b2ec77a3af5cd3ff13364" + }, + { + "path": "src/shared/system-cli-install-dirs.ts", + "sha256": "635b59f51022c9f31bf7720385b343018d4ce849220d422115e74f5351a9f21d" + }, + { + "path": "src/shared/node-cli-command-resolution.ts", + "sha256": "5eecee0c0d6296b962315c2ef2ae228a1bb1b6a7449f46d5afe3977f0af1e145" + }, + { + "path": "src/main/codex/codex-app-server-process-tree-kill.ts", + "sha256": "0a370b1d40509aa967634d6551f6baf5462b648910ddb79c2624555a84116b0b" + }, + { + "path": "src/shared/main-process-ndjson-framer.ts", + "sha256": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490" + }, + { + "path": "src/main/codex/codex-app-server-record-reader.ts", + "sha256": "b4d600293228ae2429090facc3db6f98dff449f2c3d82f35a0bea11fe4d70048" + }, + { + "path": "src/main/codex/codex-app-server-session.ts", + "sha256": "9adfbab8e229d3998d973ad9abfe647015ff21a5517fa6e5c7b32acf0a446eb8" + }, + { + "path": "src/main/codex/codex-app-server-exit-error.ts", + "sha256": "d58cdd97ae3ad4a786c7c4a0b1062ee3c5bb15b0943046dda7fff4b51ede21d1" + }, + { + "path": "src/main/codex/codex-app-server-handshake.ts", + "sha256": "3dc86d5961076713cb67ff58f593ca0fe848cd2f6b5a3a0a5da83773f49e6516" + }, + { + "path": "src/main/codex/codex-app-server-handshake-exit-proof.ts", + "sha256": "01832f6ab97bb68566221151d6a88e03cd5461f699991ad0e2c1f52573541ca1" + }, + { + "path": "src/shared/crash-reporting-diagnostic-bundle.ts", + "sha256": "2ac76a8941d4e8ae8d4e0a95ac9e10102602670e4c8fdbcb88a03b014903d1ba" + }, + { + "path": "src/shared/crash-report-signature-lines.ts", + "sha256": "564936f235f0c9faefc0c9f18f2fcbc588e5bef69ecd1071baac5a30a7c662a8" + }, + { + "path": "src/shared/posix-wait-status.ts", + "sha256": "1eb72525881baa2815548d31d2b83aaecf88e5dfd0258dbcaa5cda1e8fdcd69f" + }, + { + "path": "src/shared/crash-report-exit-code.ts", + "sha256": "a73f4e6fce72f11f5eec32c2a22d2e749f330f434aef20cec6dc03dbde55ebc6" + }, + { + "path": "src/shared/react-update-depth-attribution.ts", + "sha256": "15fd538fa4953088b63d9307fc575d4f795614b445a558340d80d61bb4c50929" + }, + { + "path": "src/shared/crash-report-attribution-lines.ts", + "sha256": "3db6cabca0444343347a90e6ec71a015a79489097e2484202cf2b57232483a6e" + }, + { + "path": "src/shared/crash-report-redaction.ts", + "sha256": "afd00bd49c77398425ee481070818e98067e90bb472a7454c571bf77ceee9c7f" + }, + { + "path": "src/shared/crash-reporting.ts", + "sha256": "e9749318038d145b7ae1819ffed6b285815c502d0eba13181c48b73b1bd3e918" + }, + { + "path": "src/main/observability/redactor.ts", + "sha256": "59190309dbef22508e3f2ef27b0190093d5aaab8cce146d6d49a89eb9d0a2b73" + }, + { + "path": "src/main/observability/tracer.ts", + "sha256": "82b6f4ec0be07b6aa612038b21b9f67b0ef199f694d29dc9f7b54b55dba9776e" + }, + { + "path": "src/main/crash-reporting/crash-breadcrumb-store.ts", + "sha256": "8848be8a897e0d1a214dbbaf87a0377d493ee83358763c0fc286e693889b2cb9" + }, + { + "path": "src/main/crash-reporting/main-process-lifecycle-identity.ts", + "sha256": "797cd37e5af6ec71f91636bc779274572a62ea67e5fa2874bb1f2f771020c950" + }, + { + "path": "src/main/crash-reporting/durable-crash-breadcrumb.ts", + "sha256": "477e28157e119f3bd85bc77d4adb185d36e44103b8a087d680715ce71b05dea7" + }, + { + "path": "src/main/crash-reporting/self-initiated-tree-kill-log.ts", + "sha256": "eb28b4a513a3cbc30586df30569adf29d828f059522ebe6c751d55b233c13987" + }, + { + "path": "src/shared/app-environment.ts", + "sha256": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140" + }, + { + "path": "src/main/orca-chromium-process-pids.ts", + "sha256": "960a15f005ff997fa379972a8c2abe3410b78c6facee449e8a5350754cfce965" + }, + { + "path": "src/main/own-chromium-tree-kill-guard.ts", + "sha256": "3a59cccd8cf923506aacad58f1c5d11b49f47a21e6c6dee8559912ee3c5cefb5" + }, + { + "path": "src/main/windows-process-tree-kill.ts", + "sha256": "83236378e93e3a15189ca02febf6b9b94d9d7eadafb46f0fcb72051904e6f45d" + }, + { + "path": "src/shared/process-table-index.ts", + "sha256": "30f236b7d0275f05673c0a7982bb0aa67c9319762749f5a583c5c31e51e31f36" + }, + { + "path": "src/shared/process-table-snapshot.ts", + "sha256": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + }, + { + "path": "src/shared/process-table-snapshot-reader.ts", + "sha256": "7d35601a2a7bf7a9c5b4a897995e0e8a15775b4d35939f3dd1abe1a042bb1c5e" + }, + { + "path": "src/main/windows/windows-command-line-recovery-health.ts", + "sha256": "a135da3611d54fecb0dba52b4d81e06ca1532680424b55e7efa315b7c886ec33" + }, + { + "path": "src/shared/child-process/windows-system-binary.ts", + "sha256": "0c42c7c454534a0937f4153e9708aeb243bd930d130ffc6493ac42d21405606e" + }, + { + "path": "src/main/windows/windows-process-table-cim-scan.ts", + "sha256": "ecfa487a3139cc087eae78ff61bf358f7eb62aec3b20e9d064d96b6e37862ab0" + }, + { + "path": "src/main/windows/windows-process-table.ts", + "sha256": "de0565f724f49ca393bb55c1618e697ea34e98bfee132c196d0c3e97713aabe1" + }, + { + "path": "src/main/providers/windows-foreground-process-rows.ts", + "sha256": "374accddf905f5b60fb1be4010e8160bfaf7f1a3834ecaffbf0119e555c7a765" + }, + { + "path": "src/main/windows-pty-root-identity.ts", + "sha256": "5e1ec017199f1a78150064d22eca4fd724874473c042f78fa236f3831f761711" + }, + { + "path": "src/main/pty-descendant-termination.ts", + "sha256": "dbdfe91d9248b4fbfbdac1733237ff3a4776e596a6a127fbdf5c4fb1d987b61e" + }, + { + "path": "src/main/pty-descendant-exit-verification.ts", + "sha256": "1f7a14a8273a228180538b3d37a20e0631e60a2e497134de499907eabfeb9b77" + }, + { + "path": "src/main/runtime/agent-session-process-identity-probe.ts", + "sha256": "4f6bc0fc286f74b07a26bdfbe69a9124498deb289c5d96db8b1334e12fdcce33" + }, + { + "path": "src/main/codex/codex-structured-owner-identity.ts", + "sha256": "b5b286f638515cae36539d0061c3808e64e6b2ed464f9326255fcd2f655d85f7" + }, + { + "path": "src/main/runtime/agent-session-spawn-token-readback.ts", + "sha256": "1b2481a5487b9ee7e21a2103b468e69d91879f84a2a330b7fa81c7a3b056e399" + }, + { + "path": "src/main/runtime/agent-session-spawn-token-process-scan.ts", + "sha256": "57fe5ac196d53e266b4d5e9ea0fab7311a292b5f281cbe56eea98c5579634bfc" + }, + { + "path": "src/main/codex/codex-app-server-process-teardown.ts", + "sha256": "183110df0c529c8eaec77ed457a9b17c5322d313482119f488f019dbf6d73f75" + }, + { + "path": "src/main/codex/codex-app-server-frame-size-error.ts", + "sha256": "8f567def6e41edb29c3a56b2be4377f64a99999c76bf24be2980a296ce6c01e5" + }, + { + "path": "src/main/codex/codex-app-server-jsonl.ts", + "sha256": "428b802a6704ec57efb26c696ad9e77da727a990b881665fdf5f68542cf143b4" + }, + { + "path": "src/main/codex/codex-app-server-request-error.ts", + "sha256": "67140be11e2b29f93d8a352552af733d228ac1808b6cf53bb7227e3906794eea" + }, + { + "path": "src/main/codex/codex-app-server-record-prefix.ts", + "sha256": "e70f8492995568e2891af0f58a78e5eb9a2f5f1970b9a561aa1bd7af32673f0d" + }, + { + "path": "src/main/codex/codex-app-server-record-dispatch.ts", + "sha256": "48554c6f9d4cb056d1a8e2e495b1c3fdadecf891d8f02d9599a7af3989f6ab23" + }, + { + "path": "src/main/codex/codex-app-server-connection.ts", + "sha256": "30fcca9f4cda5d6cceb9215905d7f3ead2361764b80e0e67ba0bcbb55e00672d" + }, + { + "path": "src/main/codex/codex-structured-thread-facts.ts", + "sha256": "a244c847c5ca22c3e3fac3b075b51f0a566f8cf6726731f69f50f7eb079b063a" + }, + { + "path": "src/main/codex/codex-structured-turn-processes.ts", + "sha256": "6a942ed29995d8001f8c041c693ed7bd30ecfc3fac5dd30e4c5ec70c0a12396a" + }, + { + "path": "src/main/codex/codex-structured-turn-cancellation.ts", + "sha256": "518b8e5a2f6dc4f8e46e01366c0a61830d6c805b68101484726bd62bbfc831a8" + }, + { + "path": "src/shared/orca-dispatch-status-prompt.ts", + "sha256": "fd9fedb3c839cd9d17b545329937c8fb6b9fa081120eb4678a9fb66f4252629e" + }, + { + "path": "src/shared/agent-status-field-normalization.ts", + "sha256": "a60c119eade7026c9e6f37ff98e77e718bd0cb41e929b0e256872981411e3328" + }, + { + "path": "src/main/native-chat/agent-session-wire/provider-frame-activity.ts", + "sha256": "9888661f598cee4d622f22b208f9ba1fc6615df6424328cf818cebe9fe86b8d7" + }, + { + "path": "src/main/codex/codex-subagent-activity.ts", + "sha256": "5cdf18a4ea2c68178a67a845d4c95672a0d94bb282e8e15c3a930c6dd59151e4" + }, + { + "path": "src/shared/native-chat-types.ts", + "sha256": "ca9440aba61822a8b92cc1009790d5f6e7bdb2f570cf159349a42027dfd40def" + }, + { + "path": "src/shared/native-chat-subagent-summary.ts", + "sha256": "a2cdf19a07edf3b50a971af22044608d50ea9cfebd07abc01af38755a2143077" + }, + { + "path": "src/main/codex/codex-subagent-executions.ts", + "sha256": "2a356e9a108a302e1131d40112401a48c582b2d5560eab073004a7efefde5259" + }, + { + "path": "src/main/codex/codex-subagent-group-body.ts", + "sha256": "9ff41e055ee33fb556f2079604271f13e696ca0114eaaa9a5f25166e5fa49280" + }, + { + "path": "src/main/codex/codex-structured-journal-limits.ts", + "sha256": "2fd2d5015b73fb948df6abdb6469039b1ee3a680db5119b1a44ebf82bc152492" + }, + { + "path": "src/main/codex/codex-subagent-roster.ts", + "sha256": "bafd291665fb178b0912bd2a2723910c00890f0766aae7f70f8bed5d417542b1" + }, + { + "path": "src/shared/native-chat-turn-status.ts", + "sha256": "4ce2b64fa7818106814c135e5daf77bb183f601c35f9f08e5b7ff847fba01378" + }, + { + "path": "src/shared/native-chat-tool-identity.ts", + "sha256": "8c98eec1a53b26a67ed48a0859c5863f5accbe67e013c4eb5a0f8b32441ee925" + }, + { + "path": "src/main/codex/codex-goal-journal-rows.ts", + "sha256": "415846ca105300349778da7637216ca6ce978c7036219f6b3038f7228ba8724e" + }, + { + "path": "src/main/native-chat/agent-session-wire/provider-frame-disposition.ts", + "sha256": "f24008cebb1b8a77b0cbd091e49140a4bf99c676bc61eedeb0a82f1263244a7c" + }, + { + "path": "src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts", + "sha256": "f8169a024a573adfc97e4eb8c99ecc4e80f82e9f94a04a29011a6b8051cc7626" + }, + { + "path": "src/shared/raster-image-dimensions.ts", + "sha256": "0d7462b8b2e53bfcdcbd32803edcfb85ed0063f56d45cf230fb59aac4783d063" + }, + { + "path": "src/shared/raster-image-preview-limits.ts", + "sha256": "f2d836b354b3951912f1ec689d84b82aa1aade89ed8e9c6f76be2321d600e9a7" + }, + { + "path": "src/shared/raster-image-base64-preview.ts", + "sha256": "89baf40f4637615456888e0f35115517a8445a3b1cb640b7bc495dd9a98fcedb" + }, + { + "path": "src/shared/image-data-uri.ts", + "sha256": "3eb9d2bda499b8f10c74783f9de766041469b140a9b96b2b5be7d97528688ce0" + }, + { + "path": "src/main/codex/codex-image-item-translation.ts", + "sha256": "c4f88236c707472cbcfcdfb1a04e51c738f8741aa5847a33f730759499522f5e" + }, + { + "path": "src/main/codex/codex-command-action-class.ts", + "sha256": "93b7bceb66a9e5bfae680e018b61bddb5c1117fe26243088ceccc8738841f966" + }, + { + "path": "src/main/codex/codex-thread-item-identity.ts", + "sha256": "04b98b367a4d658fb759ebc4fc73ea4ae731ae3f238f191c8ce002b2a2c9e2a8" + }, + { + "path": "src/main/codex/codex-turn-ordinals.ts", + "sha256": "9b7cf66986235bcacd6c198950452c7f40bd559a3a0ce20995aac496a1e32d7f" + }, + { + "path": "src/main/codex/codex-structured-item-translation.ts", + "sha256": "8ab9debb279baf3a5043ffe754c8911755c6e4ca9edfc0a094671c4d5d166639" + }, + { + "path": "src/main/codex/codex-structured-journal-contracts.ts", + "sha256": "59dbe0f4f38aea3ff31ae62d7a8d244636e2813bb4a0f1e1c2db3362bdf193ac" + }, + { + "path": "src/main/codex/codex-structured-journal-generic-frames.ts", + "sha256": "5fe2414993c44075d9c979d76091c89611360cb166f650e4483d1e6bad3b30d2" + }, + { + "path": "src/main/native-chat/agent-session-wire/structured-session-compaction.ts", + "sha256": "c73d272d59d71522d3d61eea66cf61da205c739743d720d15cb70f78bd5e07ac" + }, + { + "path": "src/shared/agent-session-journal-item-key.ts", + "sha256": "09ebe4758e3f38b5b7591f07f7b4dbfd04c7cc01713c9b0afe03b07b656d1fe8" + }, + { + "path": "src/shared/agent-session-journal-types.ts", + "sha256": "4f85b301aa12ea14ac87b0099135247ad0758529cdb2ce003bb6a1031cf279ea" + }, + { + "path": "src/shared/agent-session-journal-schemas.ts", + "sha256": "579ab7671b7f8e46374f11b6ac669bf659bd5505345c49d7dd1c9acc4b0ef564" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-row-schema.ts", + "sha256": "de9c4f6324feef96fd33cfb0698f4c34d7e02694809fb133757146ead0cbb9c3" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.ts", + "sha256": "9361c2255ac501b4c18baf22dd4e74936bc8c1181f227329b97bf1af7593bf4c" + }, + { + "path": "src/main/codex/codex-structured-journal-sink.ts", + "sha256": "e05226f2cb906557a0811de780b9f30e4bbedbd6253cd4948e5518646b63cf26" + }, + { + "path": "src/main/codex/codex-structured-journal-compactions.ts", + "sha256": "45c8e450be19cf8b88b5ffa90311e80390f40a32fd2ceaddf6bfd4cf7992ba03" + }, + { + "path": "src/main/codex/codex-goal-journal-identity.ts", + "sha256": "55d5c3a3120ea038aac9ae64cd51c5bfdf0e13af1306dca01009993cf482ad08" + }, + { + "path": "src/main/codex/codex-structured-journal-goals.ts", + "sha256": "d14794482031e86cf21f2c859ce8097f0759a5a0b2bc8f97ea26598ecfd190a5" + }, + { + "path": "src/shared/agent-turn-lifecycle-text.ts", + "sha256": "bf3a91d9c5157be19c9c88aed96e2c4e076e744362b34ddf2b466c707067e32a" + }, + { + "path": "src/shared/agent-session-turn-record.ts", + "sha256": "1a3df88a627b4744ccbe8168860033dfef66bd4ff028c6431af6a6ac2f63194f" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-terminal-settlement.ts", + "sha256": "9344b1593bf91a49a6e1d2fb9604e2890939eb1b1f0cadbbdae7194db57a8080" + }, + { + "path": "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts", + "sha256": "48828e4ee21858075cbb87ec1caa4a82991a55f80928482615d1df7ec5dc0fb2" + }, + { + "path": "src/main/codex/codex-command-lifecycle.ts", + "sha256": "7b480d3b111304e3caff71768fb3b11ab6c36e1fa8f5b0440623a77cac2382c2" + }, + { + "path": "src/main/codex/codex-structured-item-stream-bounds.ts", + "sha256": "88e9efa8a9c163905f1a5eb5eaf316fba2f194d76c5a94ed7f55636cc0d74ae0" + }, + { + "path": "src/main/codex/codex-item-stream-retention.ts", + "sha256": "66d1f1d51ed6711e20f81698b14be2d003e899dbc587ac1306b6a656cf951b96" + }, + { + "path": "src/main/codex/codex-structured-item-stream-events.ts", + "sha256": "da2a9da9025af354ce705a190badadf822f9ac3b403eef04aa0ef8aaf6f023de" + }, + { + "path": "src/main/codex/codex-structured-item-streams.ts", + "sha256": "b70c3306e8bec3587342afc3bc156f40903513fa7ff3865b217ba4cc912cd89f" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-values.ts", + "sha256": "fb66d47e32f4040f1b248a886b077506d23d0492264412e6cc3c96b579de425d" + }, + { + "path": "src/main/codex/codex-structured-dispatch-echo.ts", + "sha256": "4be5e3b55bd0a5507878798f221e758444dc82ede2f14e741f08b6b85979cd7a" + }, + { + "path": "src/main/codex/codex-structured-journal-items.ts", + "sha256": "7a9dc981cdcb1931d620feaa5523b486600c50c241f223ffdc71aa9edeb4a5bb" + }, + { + "path": "src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts", + "sha256": "e4382b9d5b8a9cdba94b2ee71188119790987b11fd0f8f9718ff4be899e9a9fa" + }, + { + "path": "src/main/codex/codex-structured-prompt-items.ts", + "sha256": "813e8fdf2768663e31822699a5a7311df8e714ca2a269dcbb1f42e8d196ac456" + }, + { + "path": "src/main/codex/codex-structured-journal-prompts.ts", + "sha256": "806f61f913c28593b02b6ea6dcb68559b126bf939b91b1e5cf4f43fedabf45a2" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turns.ts", + "sha256": "9042b2393d7d8898ed4f7436669dfbc82504e15b9ea90eb00e1518ee4db254e0" + }, + { + "path": "src/main/codex/codex-structured-journal-settlement.ts", + "sha256": "761616c292e15894b337fdd171acd7c2c1272fcabdbad3ea460f8831a923c797" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-frames.ts", + "sha256": "0679aea6b6fc274448d007c292218f90bfdd8ad3ed4ce4a7d59d0a15aae9e455" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-restore.ts", + "sha256": "a2e521b7cae677a5b3fd671065e5e4b1c812e2444f272df0f79feddae3640652" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turn-boundaries.ts", + "sha256": "bb482b2c49fa3754b76e1e749185bb2804409e017f909c0453022ead07f99a03" + }, + { + "path": "src/main/codex/codex-structured-journal-translation-turn-state.ts", + "sha256": "f6dd637f0e77f193923c3f3843b86d12786b75b9ce76e5a7e057a7187a832fe5" + }, + { + "path": "src/main/codex/codex-structured-journal-translation.ts", + "sha256": "56e17b9649471e804d29d1d54a3e14b4298ca8af2e25c4759dc37fce000b06a1" + }, + { + "path": "src/main/codex/codex-server-request-disposition.ts", + "sha256": "ddd362e00a38ff0166d35bc4cca51e4f79e377f188a9e07f5966da7c056854a0" + }, + { + "path": "src/shared/node-bounded-json-stringify.ts", + "sha256": "fcadabc537aa3125d1ee514026ecca5b8e2a76aabca7e29f08d3d35008001622" + }, + { + "path": "src/shared/remote-runtime-client-error.ts", + "sha256": "593ff4ee6466c4b18b69f65a20631967205fb8e1bd5e46239cf7f526b764156a" + }, + { + "path": "src/shared/remote-runtime-memory-limits.ts", + "sha256": "862a317d7f564a2cc54cfa60b6c093de5dc8a61714eac45daa178608c1f6acb1" + }, + { + "path": "src/main/native-chat/agent-session-wire/agent-session-history-page-bounds.ts", + "sha256": "57e49a0ea59cdccf4a4fc1532d43d2c127b077233602139b2d36d79fd547fc9d" + }, + { + "path": "src/main/codex/codex-structured-rewind.ts", + "sha256": "710115f53bc673f7d7259ec9e1d6b02e7827c348225c1e6a2006ed5eaf06e6bc" + }, + { + "path": "src/main/codex/codex-structured-provider-events.ts", + "sha256": "a411dc378e521ea8fc915e2a2a21476341a30f440e8233ae5649205d2a1e2e44" + } + ] + } + } +} diff --git a/docs/audits/codex-prompt-claim-retention/reproduce.cjs b/docs/audits/codex-prompt-claim-retention/reproduce.cjs new file mode 100644 index 00000000000..9ffb5034373 --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/reproduce.cjs @@ -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) +}) diff --git a/docs/audits/codex-prompt-claim-retention/scenario.cjs b/docs/audits/codex-prompt-claim-retention/scenario.cjs new file mode 100644 index 00000000000..8a2b9de0b0e --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/scenario.cjs @@ -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 diff --git a/docs/audits/codex-prompt-claim-retention/source-versions.json b/docs/audits/codex-prompt-claim-retention/source-versions.json new file mode 100644 index 00000000000..12b24d3a1d7 --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/source-versions.json @@ -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" + } + ] +} diff --git a/docs/audits/codex-prompt-claim-retention/sources.cjs b/docs/audits/codex-prompt-claim-retention/sources.cjs new file mode 100644 index 00000000000..39fd73fbabf --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/sources.cjs @@ -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 } +} diff --git a/docs/audits/codex-prompt-claim-retention/validation.json b/docs/audits/codex-prompt-claim-retention/validation.json new file mode 100644 index 00000000000..d32b06104e8 --- /dev/null +++ b/docs/audits/codex-prompt-claim-retention/validation.json @@ -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 +} diff --git a/src/main/codex/codex-prompt-registry-retention.test.ts b/src/main/codex/codex-prompt-registry-retention.test.ts new file mode 100644 index 00000000000..78e10cbcc77 --- /dev/null +++ b/src/main/codex/codex-prompt-registry-retention.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest' +import { AGENT_SESSION_ID_MAX_LENGTH } from '../../shared/agent-session-wire' +import { CodexPromptRegistry, type CodexPendingPrompt } from './codex-prompt-registry' + +function registerPrompt( + registry: CodexPromptRegistry, + index: number, + threadId = 'thread', + turnId: string | null = 'turn', + itemId = `item-${index}` +): { itemId: string; prompt: CodexPendingPrompt } { + const prompt = registry.register({ + id: index, + method: 'item/commandExecution/requestApproval', + params: { itemId, threadId, turnId } + }) + if (!prompt) { + throw new Error('Fixture prompt was refused') + } + const journalItemId = `journal:${threadId}:${itemId}` + registry.bindJournalItemId(journalItemId, threadId, itemId, turnId) + return { itemId: journalItemId, prompt } +} + +function claimPrompt( + registry: CodexPromptRegistry, + index: number, + turnId = 'turn', + itemId?: string +): WeakRef { + const registered = registerPrompt(registry, index, 'thread', null, itemId) + registry.bindJournalItemId(registered.itemId, 'thread', registered.prompt.promptKey, turnId) + if (!registry.claimBound(registered.itemId)) { + throw new Error('Fixture prompt could not be claimed') + } + return new WeakRef(registered.prompt) +} + +async function collect(): Promise { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + for (let round = 0; round < 5; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } +} + +function evictLookupEntries(registry: CodexPromptRegistry): void { + for (let index = 0; index < 256; index += 1) { + registerPrompt(registry, index + 1_000, 'other-thread', 'other-turn') + } +} + +describe('Codex prompt claim lifetime', () => { + it('releases 32 evicted claims only when their exact turn completes', async () => { + const registry = new CodexPromptRegistry() + const prompts = Array.from({ length: 32 }, (_, index) => claimPrompt(registry, index)) + evictLookupEntries(registry) + expect(registry.sizes).toEqual({ prompts: 128, journalBindings: 256 }) + expect(registry.find('journal:thread:item-0')).toBeNull() + registry.clearTurn('other-thread', 'turn') + registry.clearTurn('thread', 'other-turn') + await collect() + expect(prompts.filter((prompt) => prompt.deref() !== undefined)).toHaveLength(32) + + registry.clearTurn('thread', 'turn') + await collect() + expect(prompts.filter((prompt) => prompt.deref() !== undefined)).toHaveLength(0) + expect(registry.sizes).toEqual({ prompts: 128, journalBindings: 256 }) + registry.clear() + }) + + it('preserves a replacement prompt and its active claim when the old turn completes', async () => { + const registry = new CodexPromptRegistry() + const old = claimPrompt(registry, 1, 'old-turn', 'same-item') + const replacement = registerPrompt(registry, 2, 'thread', 'new-turn', 'same-item') + const claim = registry.claimBound(replacement.itemId) + if (!claim) { + throw new Error('Replacement prompt could not be claimed') + } + registry.clearTurn('thread', 'old-turn') + await collect() + expect(old.deref()).toBeUndefined() + expect(registry.find(replacement.itemId)).toBe(replacement.prompt) + expect(registry.ownsBoundClaim(claim, replacement.itemId, 'thread', 'new-turn')).toBe(true) + registry.clearTurn('thread', 'new-turn') + expect(registry.ownsClaim(claim)).toBe(false) + }) + + it('finds an evicted claim through its bounded turn digest', async () => { + const registry = new CodexPromptRegistry() + const turnId = 'x'.repeat(AGENT_SESSION_ID_MAX_LENGTH + 1) + const prompt = claimPrompt(registry, 1, turnId) + evictLookupEntries(registry) + registry.clearTurn('thread', turnId) + await collect() + expect(prompt.deref()).toBeUndefined() + registry.clear() + }) + + it('releases evicted claims when the session is cleared', async () => { + const registry = new CodexPromptRegistry() + const prompt = claimPrompt(registry, 1) + evictLookupEntries(registry) + registry.clear() + await collect() + expect(prompt.deref()).toBeUndefined() + }) +}) diff --git a/src/main/codex/codex-prompt-registry.ts b/src/main/codex/codex-prompt-registry.ts index f3ba3fa3601..059f8a7a0d3 100644 --- a/src/main/codex/codex-prompt-registry.ts +++ b/src/main/codex/codex-prompt-registry.ts @@ -226,7 +226,7 @@ export class CodexPromptRegistry { 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) ) ) From 79800e60b4ba886da28bd65eaf9edbd264f570c7 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:13 -0700 Subject: [PATCH 14/59] fix: release completed terminal spawn inputs (#21139) Co-authored-by: m4air --- .../terminal-completed-spawn-inputs/README.md | 71 ++++++ .../admission-control.cjs | 172 ++++++++++++++ .../admission-electron.json | 84 +++++++ .../admission-node.json | 84 +++++++ .../baseline.config.mjs | 23 ++ .../electron-baseline.json | 66 ++++++ .../electron-fixed.json | 66 ++++++ .../terminal-completed-spawn-inputs/fix.patch | 122 ++++++++++ .../mapped-admission-electron.json | 84 +++++++ .../mapped-admission-node.json | 84 +++++++ .../mapped-electron-baseline.json | 66 ++++++ .../mapped-electron-fixed.json | 66 ++++++ .../mapped-node-baseline.json | 66 ++++++ .../mapped-node-fixed.json | 66 ++++++ .../node-baseline.json | 66 ++++++ .../node-fixed.json | 66 ++++++ .../reproduce.cjs | 208 +++++++++++++++++ .../source-versions.json | 113 +++++++++ .../spawn-fixture.cjs | 81 +++++++ .../spawn-source.cjs | 114 ++++++++++ src/main/daemon/session-output-pipeline.ts | 5 +- .../daemon/terminal-host-session-create.ts | 14 +- ...erminal-host-spawn-input-retention.test.ts | 214 ++++++++++++++++++ src/main/daemon/terminal-host.ts | 34 +-- 24 files changed, 2015 insertions(+), 20 deletions(-) create mode 100644 docs/audits/terminal-completed-spawn-inputs/README.md create mode 100644 docs/audits/terminal-completed-spawn-inputs/admission-control.cjs create mode 100644 docs/audits/terminal-completed-spawn-inputs/admission-electron.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/admission-node.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/baseline.config.mjs create mode 100644 docs/audits/terminal-completed-spawn-inputs/electron-baseline.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/electron-fixed.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/fix.patch create mode 100644 docs/audits/terminal-completed-spawn-inputs/mapped-admission-electron.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/mapped-admission-node.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/mapped-electron-baseline.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/mapped-electron-fixed.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/mapped-node-baseline.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/mapped-node-fixed.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/node-baseline.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/node-fixed.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/reproduce.cjs create mode 100644 docs/audits/terminal-completed-spawn-inputs/source-versions.json create mode 100644 docs/audits/terminal-completed-spawn-inputs/spawn-fixture.cjs create mode 100644 docs/audits/terminal-completed-spawn-inputs/spawn-source.cjs create mode 100644 src/main/daemon/terminal-host-spawn-input-retention.test.ts diff --git a/docs/audits/terminal-completed-spawn-inputs/README.md b/docs/audits/terminal-completed-spawn-inputs/README.md new file mode 100644 index 00000000000..bb8bae2dfbc --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/README.md @@ -0,0 +1,71 @@ +# Completed terminal spawns retain consumed inputs + +Status: reproduced against actual `TerminalHost`, `Session`, output pipeline, and daemon admission code on Node 26.6.0 and installed Electron 43.7.0 / Node 24.21.0. The fix releases completed request objects and consumed history seed arrays while the terminal remains alive. + +## Retaining paths and fix + +Three long-lived callbacks kept spawn-only input objects reachable: + +1. `terminal-host-session-create.ts::spawnAndPublishSession` gave `Session` an exit callback capturing the complete request and dependencies. A small factory now captures only the exit callback, session ID, and agent-session generation. +2. `TerminalHost.createOrAttach` constructed that exit callback beside the cancellation check that captures the request. Their shared lexical context kept the request reachable even after the first capture was projected. The unchanged exit/reap body now lives in a method bound to its host. +3. `session-output-pipeline.ts` captured pipeline options in its foreground-confirmation callback. Those options include history chunks that `SessionOutputPlane` has already consumed synchronously. The callback now captures the subprocess object; the liveness callback is also extracted before constructing the pipeline. + +The subprocess remains the receiver of `subprocess.confirmShellForeground?.()`. The only production provider of the exit callback is `TerminalHost`; its bound method preserves the host receiver. Exit codes, incarnation tombstones, claimed-generation release, reaping, cancellation, and process ownership follow the same paths. A constructor-only `maxTombstones` field was removed to keep `TerminalHost` within the existing line limit; the registry receives the same configured/default value directly. + +## Production reachability and limits + +- `daemon-provider-init.ts::initDaemonPtyProvider` installs the local daemon adapter. The cold-restore path in `daemon-pty-spawn-result.ts` supplies recovered history to terminal creation. `daemon-server.ts` owns the host and admission objects; `daemon-request-router.ts:59` routes `createOrAttach` to admission. +- `daemon-terminal-admission.ts:90` obtains inline history or takes completed transfer chunks, then passes the chunks, environment, and cancellation inputs into the host at line 96. `session-output-plane.ts:63` consumes all seed chunks into the emulator and retains the success flag. +- `terminal-history-seed-transfer-registry.ts:97` removes a completed transfer from its map and byte accounting when handing its chunks to creation. Its pending-transfer limits therefore do not bound the aggregate of already-consumed seeds retained by live sessions. The configured checkpoint maximum is 200,000,000 bytes, but these proofs use tiny seeds and do **not** measure a 200 MB allocation or incident-sized RSS. +- Retention lasts for the live session. Disposal permits collection even before the fix. This is avoidable retention per live terminal, not proof of unlimited growth after successful teardown. +- Real admission stream callbacks still keep preparation/signal metadata while attached: the routed-session getter shares the admission context with its cancellation callback (`daemon-terminal-admission.ts:117–122`). The admission control confirms those objects collect after public `host.detach` with the fix. This patch does not change that attached-stream lifetime. +- Native `pty-subprocess/subprocess-handle.ts:48–60` still captures its spawn arguments through the exit-status callback, including its merged environment object. Collection of the original request environment object does not prove all copied environment strings disappear from a real native process owner. The proof injects an inert subprocess and does not measure native allocations. +- The daemon path can run locally and on execution hosts used remotely. No wire fields or messages change, and folder workspaces require no special behavior. The finding is compatible with a local application memory report such as #19831, but no affected-host process/heap evidence establishes that the incident used this restore path or that it explains the reported magnitude. + +## Reproduction and controls + +`spawn-source.cjs` bundles actual source and reconstructs the baseline in memory by reversing `fix.patch`. SHA-256 checks fence both versions of all three changed modules using `source-versions.json`. The loader accepts the exact audit-branch pair and the exact independent-main publication pair; all other source hashes fail. Reports contain hashes of the source actually evaluated. Dependencies remain actual worktree code. Only the OS descendant-kill port is replaced with a throwing guard; subprocess handles are small injected objects, with no real shell, socket, process signal, or network activity. + +`reproduce.cjs` measures weak references to request, environment, history array, and cancellation signal objects. It also tests pending ownership, actual exit/reap and claimed-generation replacement, retired-incarnation exit evidence, and foreground confirmation with the correct subprocess receiver and queued prompt delivery. + +| Check | Baseline | Fixed | +| --------------------------------------------------------- | ------------------------------- | -------------------- | +| Three completed requests while three sessions remain live | 3 of each input object retained | 0 of each retained | +| One request during unresolved spawn | All four input objects retained | All four retained | +| That request after publication | All four retained | All four collectible | +| Inputs after disposal | All collectible | All collectible | +| Exit/reap, new incarnation/generation, shell confirmation | Pass | Pass | + +`admission-control.cjs` exercises actual daemon admission and preparations above the actual host. A forwarding observer stores only weak references. Transport, attachment bookkeeping, and native subprocess ports are inert. Both runtimes reproduce the following: + +| Admission phase | Original options/env/history | Preparation/signal | Request/payload | +| ------------------------------------------- | ---------------------------- | ------------------ | --------------- | +| Baseline, attached or detached live session | Retained | Retained | Collectible | +| Fixed, attached live session | Collectible | Retained | Collectible | +| Fixed, detached live session | Collectible | Collectible | Collectible | +| Either version after disposal | Collectible | Collectible | Collectible | + +The seeded snapshot remains readable after collection. These object reachability checks establish specific removed retaining paths; they do not establish total memory released. No heap-snapshot tool was exposed in this session. The historical Electron 43.4.1 binary was not tested. Each process uses a 192 MiB old-space limit and a 15-second deadline. + +Run from the worktree: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/terminal-completed-spawn-inputs/reproduce.cjs --baseline +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/terminal-completed-spawn-inputs/reproduce.cjs +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/terminal-completed-spawn-inputs/admission-control.cjs +ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/daemon/terminal-host-spawn-input-retention.test.ts +``` + +For Electron, run the same proof scripts with the binary returned by `require('electron')`, the same Node flags, `ELECTRON_RUN_AS_NODE=1`, and `ORCA_BACKGROUND_LAUNCH=1`. This starts no application or window. Node and Electron reports are stored separately in this directory. + +The four permanent regressions pass with the fix. The reconstructed baseline deliberately fails the two retention regressions and passes both lifecycle controls: `ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config docs/audits/terminal-completed-spawn-inputs/baseline.config.mjs` exits 1. Existing host, concurrent create, teardown/recreate, reaping, agent ownership, preflight replacement, and history restore tests also pass: 67 tests across nine files. Node typecheck and the changed-code quality gate passed; explicit basic/type-aware lint includes the audit scripts. + +## Source identity and compatibility + +`source-versions.json` records the exact audited branch baseline, fixed hashes, previously reviewed main commit `77cd61df396f25ec91ee2d5ddcbd1f55aa94f818`, release `v1.4.198` commit `e0826956fcfc532f5a1e55b5e081f2e57e553c43`, and independent publication main commit `291b4ddd6f1c1af480169885e0fda7f9c78ff053`. The create and pipeline files exactly match these historical baselines. Historical `TerminalHost` differs only in the unrelated producer pause/resume source parameter from #20947 on the audit branch. This fix applies independently and does not require #20947. + +The supported `TerminalHost` SHA-256 pairs are audit baseline `8cd6804b249ffb0d82da7f5a7ec8faa66514b0d6e3e36472cd81e6409c3edbc4` → fixed `23cfd9c6317db30edd48a0c8bd7c54658206654703461cf12295274b23fb8844`, and independent-main baseline `5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca` → fixed `f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f`. Each selected fixed source is reverse-patched and checked against its own paired baseline hash. + +The four permanent tests pass when the three patched main modules are overlaid on current dependencies. The six `mapped-*.json` reports repeat both runtime proofs and admission controls using the exact patched publication-main modules. They report the main hashes actually evaluated. This is a narrow compatibility check with working-tree dependencies, not a full historical application build. An optional `ORCA_SPAWN_INPUT_PROOF_SOURCE_MAP` points to a JSON object from these three relative source paths to exact reviewed fixed-source strings; unknown or incomplete mappings fail the same hash checks. With no mapping, the loader checks the published checkout directly. Mapped runs write separate reports prefixed `mapped-`. + +Cancellation wait/listener findings from the preceding audit remain diagnostic and are outside this patch. The independent admission review narrowed the signal/environment claims before publication. diff --git a/docs/audits/terminal-completed-spawn-inputs/admission-control.cjs b/docs/audits/terminal-completed-spawn-inputs/admission-control.cjs new file mode 100644 index 00000000000..de421ae36ad --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/admission-control.cjs @@ -0,0 +1,172 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { + loadExports, + evaluatedSourceHashes, + sourceMode, + reportPrefix, + sha +} = require('./spawn-source.cjs') +const { subprocess, collect } = require('./spawn-fixture.cjs') + +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function') +const root = path.resolve(__dirname, '../../..') + +function observeOptions(refs, host) { + return { + createOrAttach(options) { + refs.options = new WeakRef(options) + refs.env = new WeakRef(options.env) + refs.history = new WeakRef(options.historySeedChunks) + refs.signal = new WeakRef(options.cancelSignal) + return host.createOrAttach(options) + }, + detach: (...args) => host.detach(...args) + } +} + +function observePreparations(refs, preparations) { + return { + register(...args) { + const preparation = preparations.register(...args) + refs.preparation = new WeakRef(preparation) + return preparation + }, + prepareUnlessCanceled: (...args) => preparations.prepareUnlessCanceled(...args), + finish: (...args) => preparations.finish(...args) + } +} + +async function create(admission, refs) { + const request = { + id: 'request', + type: 'createOrAttach', + payload: { + sessionId: 'admission-review', + cols: 80, + rows: 24, + env: { REVIEW: 'request-input' }, + historySeed: 'ADMISSION-HISTORY-SEED\r\n' + } + } + refs.request = new WeakRef(request) + refs.payload = new WeakRef(request.payload) + const result = await admission.createOrAttach('client', request) + assert.equal(result.isNew, true) + assert.equal(result.historySeeded, true) +} + +async function exercise(api) { + const refs = {} + const host = new api.TerminalHost({ spawnSubprocess: async () => subprocess() }) + const preparations = new api.DaemonPtySpawnPreparations(async () => {}) + const client = { authenticatedPairEstablished: true, streamSocket: {} } + const attachments = [] + const admission = new api.DaemonTerminalAdmission({ + host: observeOptions(refs, host), + preparations: observePreparations(refs, preparations), + connections: new Map([['client', client]]), + endpoint: { hasLostOwnership: () => false }, + attachments: { + attach(...args) { + attachments.push(args) + }, + release() {}, + lastInputAt: () => undefined + }, + historySeedTransfers: { + take() { + throw new Error('Inline history only') + } + }, + transientFactRelay: { isBackgrounded: () => false, onSessionData() {}, onSessionExit() {} }, + streamDataBatcher: { + enqueue() {}, + enqueueControlEvent() {}, + flush() {}, + refreshSessionDroppability() {} + }, + log: { log() {} }, + isAcceptingWork: () => true, + requestEndpointRetirement() { + throw new Error('Unexpected endpoint retirement') + }, + reevaluateIdleShutdown() {} + }) + const retained = () => + Object.fromEntries(Object.entries(refs).map(([key, ref]) => [key, ref.deref() !== undefined])) + try { + await create(admission, refs) + assert.equal(admission.inFlight, 0) + assert.equal(preparations.pending.size, 0) + await collect() + const attached = retained() + assert.equal(host.listSessions().length, 1) + assert.match(host.getSnapshot('admission-review').snapshotAnsi, /ADMISSION-HISTORY-SEED/) + assert.equal(attachments.length, 1) + host.detach('admission-review', attachments[0][2]) + await collect() + const detached = retained() + assert.equal(host.listSessions().length, 1) + await host.dispose() + await collect() + const disposed = retained() + assert(Object.values(disposed).every((value) => !value)) + return { attached, detached, disposed, historyVisibleAfterCollection: true } + } finally { + await host.dispose() + } +} + +async function main() { + const phases = {} + for (const phase of ['baseline', 'fixed']) { + const result = await exercise(await loadExports(phase === 'fixed')) + for (const key of ['options', 'env', 'history']) { + assert.equal(result.attached[key], phase === 'baseline') + assert.equal(result.detached[key], phase === 'baseline') + } + for (const key of ['preparation', 'signal']) { + assert.equal(result.attached[key], true) + assert.equal(result.detached[key], phase === 'baseline') + } + for (const key of ['request', 'payload']) { + assert.equal(result.attached[key], false) + assert.equal(result.detached[key], false) + } + phases[phase] = result + } + const sourceHashes = { ...evaluatedSourceHashes } + for (const file of [ + 'src/main/daemon/daemon-terminal-admission.ts', + 'src/main/daemon/daemon-pty-spawn-preparations.ts' + ]) { + sourceHashes[file] = sha(fs.readFileSync(path.join(root, file))) + } + const report = { + node: process.version, + electron: process.versions.electron ?? null, + v8: process.versions.v8, + sourceMode, + sourceHashes, + phases + } + fs.writeFileSync( + path.join( + __dirname, + `${reportPrefix}admission-${process.versions.electron ? 'electron' : 'node'}.json` + ), + `${JSON.stringify(report, null, 2)}\n` + ) + console.log(JSON.stringify(phases, null, 2)) +} +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) +setTimeout(() => { + console.error('fixture timeout') + process.exit(2) +}, 15000).unref() diff --git a/docs/audits/terminal-completed-spawn-inputs/admission-electron.json b/docs/audits/terminal-completed-spawn-inputs/admission-electron.json new file mode 100644 index 00000000000..98f93204cef --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/admission-electron.json @@ -0,0 +1,84 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "sourceMode": "working-tree modules and dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": { + "baseline": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "fixed": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f" + }, + "src/main/daemon/terminal-host-session-create.ts": { + "baseline": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "fixed": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309" + }, + "src/main/daemon/terminal-host.ts": { + "baseline": "8cd6804b249ffb0d82da7f5a7ec8faa66514b0d6e3e36472cd81e6409c3edbc4", + "fixed": "23cfd9c6317db30edd48a0c8bd7c54658206654703461cf12295274b23fb8844" + }, + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/daemon-pty-spawn-preparations.ts": "77c94466578a5066af069e45eb5e87a64b73b88580a13b2172b2ea846b434f4e" + }, + "phases": { + "baseline": { + "attached": { + "request": false, + "payload": false, + "preparation": true, + "options": true, + "env": true, + "history": true, + "signal": true + }, + "detached": { + "request": false, + "payload": false, + "preparation": true, + "options": true, + "env": true, + "history": true, + "signal": true + }, + "disposed": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "historyVisibleAfterCollection": true + }, + "fixed": { + "attached": { + "request": false, + "payload": false, + "preparation": true, + "options": false, + "env": false, + "history": false, + "signal": true + }, + "detached": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "disposed": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "historyVisibleAfterCollection": true + } + } +} diff --git a/docs/audits/terminal-completed-spawn-inputs/admission-node.json b/docs/audits/terminal-completed-spawn-inputs/admission-node.json new file mode 100644 index 00000000000..84f7e83a4a1 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/admission-node.json @@ -0,0 +1,84 @@ +{ + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "sourceMode": "working-tree modules and dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": { + "baseline": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "fixed": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f" + }, + "src/main/daemon/terminal-host-session-create.ts": { + "baseline": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "fixed": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309" + }, + "src/main/daemon/terminal-host.ts": { + "baseline": "8cd6804b249ffb0d82da7f5a7ec8faa66514b0d6e3e36472cd81e6409c3edbc4", + "fixed": "23cfd9c6317db30edd48a0c8bd7c54658206654703461cf12295274b23fb8844" + }, + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/daemon-pty-spawn-preparations.ts": "77c94466578a5066af069e45eb5e87a64b73b88580a13b2172b2ea846b434f4e" + }, + "phases": { + "baseline": { + "attached": { + "request": false, + "payload": false, + "preparation": true, + "options": true, + "env": true, + "history": true, + "signal": true + }, + "detached": { + "request": false, + "payload": false, + "preparation": true, + "options": true, + "env": true, + "history": true, + "signal": true + }, + "disposed": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "historyVisibleAfterCollection": true + }, + "fixed": { + "attached": { + "request": false, + "payload": false, + "preparation": true, + "options": false, + "env": false, + "history": false, + "signal": true + }, + "detached": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "disposed": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "historyVisibleAfterCollection": true + } + } +} diff --git a/docs/audits/terminal-completed-spawn-inputs/baseline.config.mjs b/docs/audits/terminal-completed-spawn-inputs/baseline.config.mjs new file mode 100644 index 00000000000..82362685b1c --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/baseline.config.mjs @@ -0,0 +1,23 @@ +import { createRequire } from 'node:module' +import path from 'node:path' +import { defineConfig, mergeConfig } from 'vitest/config' +import rootConfig from '../../../config/vitest.config.ts' + +const require = createRequire(import.meta.url) +const { baselineSources } = require('./spawn-source.cjs') +const config = mergeConfig( + rootConfig, + defineConfig({ + plugins: [ + { + name: 'completed-spawn-input-baseline', + enforce: 'pre', + load(id) { + return baselineSources.get(path.normalize(id)) + } + } + ] + }) +) +config.test.include = ['src/main/daemon/terminal-host-spawn-input-retention.test.ts'] +export default config diff --git a/docs/audits/terminal-completed-spawn-inputs/electron-baseline.json b/docs/audits/terminal-completed-spawn-inputs/electron-baseline.json new file mode 100644 index 00000000000..448cb931379 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/electron-baseline.json @@ -0,0 +1,66 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "fixed": false, + "sourceMode": "working-tree modules and dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "src/main/daemon/terminal-host-session-create.ts": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "src/main/daemon/terminal-host.ts": "8cd6804b249ffb0d82da7f5a7ec8faa66514b0d6e3e36472cd81e6409c3edbc4" + }, + "reports": [ + { + "case": "completed-inputs", + "whileLive": { + "options": 3, + "env": 3, + "history": 3, + "signal": 3 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "liveSessionCountAtCollection": 3 + }, + { + "case": "pending-inputs", + "duringSpawn": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterPublication": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + } + }, + { + "case": "exit-and-recreate", + "exitVerdict": "exited", + "exitReason": "pty_exit_7", + "reaped": ["claimed", "claimed"], + "newIncarnation": true, + "newGeneration": true + }, + { + "case": "foreground-confirmation", + "confirmations": 1, + "preservedReceiver": true, + "owner": "shell", + "queuedPromptReleased": true + } + ] +} diff --git a/docs/audits/terminal-completed-spawn-inputs/electron-fixed.json b/docs/audits/terminal-completed-spawn-inputs/electron-fixed.json new file mode 100644 index 00000000000..3351fc4d975 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/electron-fixed.json @@ -0,0 +1,66 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "fixed": true, + "sourceMode": "working-tree modules and dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f", + "src/main/daemon/terminal-host-session-create.ts": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309", + "src/main/daemon/terminal-host.ts": "23cfd9c6317db30edd48a0c8bd7c54658206654703461cf12295274b23fb8844" + }, + "reports": [ + { + "case": "completed-inputs", + "whileLive": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "liveSessionCountAtCollection": 3 + }, + { + "case": "pending-inputs", + "duringSpawn": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterPublication": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + } + }, + { + "case": "exit-and-recreate", + "exitVerdict": "exited", + "exitReason": "pty_exit_7", + "reaped": ["claimed", "claimed"], + "newIncarnation": true, + "newGeneration": true + }, + { + "case": "foreground-confirmation", + "confirmations": 1, + "preservedReceiver": true, + "owner": "shell", + "queuedPromptReleased": true + } + ] +} diff --git a/docs/audits/terminal-completed-spawn-inputs/fix.patch b/docs/audits/terminal-completed-spawn-inputs/fix.patch new file mode 100644 index 00000000000..d18ce888276 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/fix.patch @@ -0,0 +1,122 @@ +diff --git a/src/main/daemon/session-output-pipeline.ts b/src/main/daemon/session-output-pipeline.ts +index c249e4d1d3..f67d2e15b9 100644 +--- a/src/main/daemon/session-output-pipeline.ts ++++ b/src/main/daemon/session-output-pipeline.ts +@@ -14,6 +14,7 @@ export function createSessionOutputPipeline(opts: { + subprocess: SubprocessHandle + isAlive: () => boolean + }): { output: SessionOutputPlane; recoveryBarrier: TerminalShellRecoveryBarrier } { ++ const { subprocess, isAlive } = opts + let barrier: TerminalShellRecoveryBarrier | null = null + const output = new SessionOutputPlane({ + cols: opts.cols, +@@ -24,9 +25,9 @@ export function createSessionOutputPipeline(opts: { + getTerminalOwner: () => barrier?.getOwner() + }) + const recoveryBarrier = new TerminalShellRecoveryBarrier({ +- confirmShellForeground: async () => (await opts.subprocess.confirmShellForeground?.()) ?? false, ++ confirmShellForeground: async () => (await subprocess.confirmShellForeground?.()) ?? false, + release: (emission) => output.emit(emission), +- isAlive: opts.isAlive ++ isAlive + }) + barrier = recoveryBarrier + return { output, recoveryBarrier } +diff --git a/src/main/daemon/terminal-host-session-create.ts b/src/main/daemon/terminal-host-session-create.ts +index 8f6833c3d9..fc4cc01088 100644 +--- a/src/main/daemon/terminal-host-session-create.ts ++++ b/src/main/daemon/terminal-host-session-create.ts +@@ -150,7 +150,11 @@ async function spawnAndPublishSession( + historySeedChunks: opts.historySeedChunks, + ...(opts.startupIngress ? { startupIngress: opts.startupIngress } : {}), + wslDistro, +- onExit: () => deps.onSessionExit(opts.sessionId, opts.agentSessionGeneration), ++ onExit: createSessionExitHandler( ++ deps.onSessionExit, ++ opts.sessionId, ++ opts.agentSessionGeneration ++ ), + ...(deps.reportReadinessEvent ? { reportReadinessEvent: deps.reportReadinessEvent } : {}), + ...(opts.shellReadyTimeoutMs !== undefined + ? { shellReadyTimeoutMs: opts.shellReadyTimeoutMs } +@@ -212,6 +216,14 @@ async function spawnAndPublishSession( + } + } + ++function createSessionExitHandler( ++ onSessionExit: TerminalHostSessionCreateDependencies['onSessionExit'], ++ sessionId: string, ++ generation: string | undefined ++): () => void { ++ return () => onSessionExit(sessionId, generation) ++} ++ + // Why R_OK|X_OK: listing a directory needs read, and entering it needs search — both are what + // TCC withholds. A non-permission failure (ENOENT, ENOTDIR) reads as readable so it can never + // masquerade as a permission denial. +diff --git a/src/main/daemon/terminal-host.ts b/src/main/daemon/terminal-host.ts +index 81dae092b1..1fdbb6d161 100644 +--- a/src/main/daemon/terminal-host.ts ++++ b/src/main/daemon/terminal-host.ts +@@ -54,7 +54,6 @@ export class TerminalHost { + private onSessionReaped: TerminalHostOptions['onSessionReaped'] + private reportReadinessEvent: TerminalHostOptions['reportReadinessEvent'] + private onFinalCheckpoint: TerminalHostOptions['onFinalCheckpoint'] +- private maxTombstones: number + private creationFenced = false + private disposePromise: Promise | null = null + private readonly agentSessionOwners = new ClaimedAgentPtyOwnerRegistry() +@@ -71,8 +70,7 @@ export class TerminalHost { + this.onSessionReaped = opts.onSessionReaped + this.reportReadinessEvent = opts.reportReadinessEvent + this.onFinalCheckpoint = opts.onFinalCheckpoint +- this.maxTombstones = opts.maxTombstones ?? DEFAULT_MAX_TOMBSTONES +- this.killedTombstones = new TerminalHostTombstones(this.maxTombstones) ++ this.killedTombstones = new TerminalHostTombstones(opts.maxTombstones ?? DEFAULT_MAX_TOMBSTONES) + } + + async createOrAttach(opts: InternalCreateOrAttachOptions): Promise { +@@ -123,20 +121,7 @@ export class TerminalHost { + ...(this.reportReadinessEvent + ? { reportReadinessEvent: this.reportReadinessEvent } + : {}), +- onSessionExit: (sessionId, generation) => { +- const session = this.sessions.get(sessionId) +- if (session) { +- pruneRetiredPtyIncarnations(this.retiredIncarnations) +- this.retiredIncarnations.set(sessionId, { +- incarnationId: session.incarnationId, +- code: session.exitCode ?? 0, +- expiresAt: Date.now() + REMOTE_FOREGROUND_TOMBSTONE_RETENTION_MS +- }) +- } +- this.agentSessionOwners.release(sessionId, generation) +- this.agentSessionGenerations.forget(sessionId, generation) +- this.reapSession(sessionId) +- } ++ onSessionExit: this.handleSessionExit.bind(this) + }) + } + }) +@@ -146,6 +131,21 @@ export class TerminalHost { + } + } + ++ private handleSessionExit(sessionId: string, generation: string | undefined): void { ++ const session = this.sessions.get(sessionId) ++ if (session) { ++ pruneRetiredPtyIncarnations(this.retiredIncarnations) ++ this.retiredIncarnations.set(sessionId, { ++ incarnationId: session.incarnationId, ++ code: session.exitCode ?? 0, ++ expiresAt: Date.now() + REMOTE_FOREGROUND_TOMBSTONE_RETENTION_MS ++ }) ++ } ++ this.agentSessionOwners.release(sessionId, generation) ++ this.agentSessionGenerations.forget(sessionId, generation) ++ this.reapSession(sessionId) ++ } ++ + private assertCreateOrAttachAllowed(opts: InternalCreateOrAttachOptions): void { + if (this.creationFenced) { + throw new Error('Terminal host is shutting down') diff --git a/docs/audits/terminal-completed-spawn-inputs/mapped-admission-electron.json b/docs/audits/terminal-completed-spawn-inputs/mapped-admission-electron.json new file mode 100644 index 00000000000..788cde40243 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/mapped-admission-electron.json @@ -0,0 +1,84 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "sourceMode": "mapped modules with working-tree dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": { + "baseline": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "fixed": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f" + }, + "src/main/daemon/terminal-host-session-create.ts": { + "baseline": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "fixed": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309" + }, + "src/main/daemon/terminal-host.ts": { + "baseline": "5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca", + "fixed": "f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f" + }, + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/daemon-pty-spawn-preparations.ts": "77c94466578a5066af069e45eb5e87a64b73b88580a13b2172b2ea846b434f4e" + }, + "phases": { + "baseline": { + "attached": { + "request": false, + "payload": false, + "preparation": true, + "options": true, + "env": true, + "history": true, + "signal": true + }, + "detached": { + "request": false, + "payload": false, + "preparation": true, + "options": true, + "env": true, + "history": true, + "signal": true + }, + "disposed": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "historyVisibleAfterCollection": true + }, + "fixed": { + "attached": { + "request": false, + "payload": false, + "preparation": true, + "options": false, + "env": false, + "history": false, + "signal": true + }, + "detached": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "disposed": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "historyVisibleAfterCollection": true + } + } +} diff --git a/docs/audits/terminal-completed-spawn-inputs/mapped-admission-node.json b/docs/audits/terminal-completed-spawn-inputs/mapped-admission-node.json new file mode 100644 index 00000000000..a2ec9a4d177 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/mapped-admission-node.json @@ -0,0 +1,84 @@ +{ + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "sourceMode": "mapped modules with working-tree dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": { + "baseline": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "fixed": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f" + }, + "src/main/daemon/terminal-host-session-create.ts": { + "baseline": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "fixed": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309" + }, + "src/main/daemon/terminal-host.ts": { + "baseline": "5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca", + "fixed": "f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f" + }, + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/daemon-pty-spawn-preparations.ts": "77c94466578a5066af069e45eb5e87a64b73b88580a13b2172b2ea846b434f4e" + }, + "phases": { + "baseline": { + "attached": { + "request": false, + "payload": false, + "preparation": true, + "options": true, + "env": true, + "history": true, + "signal": true + }, + "detached": { + "request": false, + "payload": false, + "preparation": true, + "options": true, + "env": true, + "history": true, + "signal": true + }, + "disposed": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "historyVisibleAfterCollection": true + }, + "fixed": { + "attached": { + "request": false, + "payload": false, + "preparation": true, + "options": false, + "env": false, + "history": false, + "signal": true + }, + "detached": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "disposed": { + "request": false, + "payload": false, + "preparation": false, + "options": false, + "env": false, + "history": false, + "signal": false + }, + "historyVisibleAfterCollection": true + } + } +} diff --git a/docs/audits/terminal-completed-spawn-inputs/mapped-electron-baseline.json b/docs/audits/terminal-completed-spawn-inputs/mapped-electron-baseline.json new file mode 100644 index 00000000000..90de8b8ec0c --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/mapped-electron-baseline.json @@ -0,0 +1,66 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "fixed": false, + "sourceMode": "mapped modules with working-tree dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "src/main/daemon/terminal-host-session-create.ts": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "src/main/daemon/terminal-host.ts": "5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca" + }, + "reports": [ + { + "case": "completed-inputs", + "whileLive": { + "options": 3, + "env": 3, + "history": 3, + "signal": 3 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "liveSessionCountAtCollection": 3 + }, + { + "case": "pending-inputs", + "duringSpawn": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterPublication": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + } + }, + { + "case": "exit-and-recreate", + "exitVerdict": "exited", + "exitReason": "pty_exit_7", + "reaped": ["claimed", "claimed"], + "newIncarnation": true, + "newGeneration": true + }, + { + "case": "foreground-confirmation", + "confirmations": 1, + "preservedReceiver": true, + "owner": "shell", + "queuedPromptReleased": true + } + ] +} diff --git a/docs/audits/terminal-completed-spawn-inputs/mapped-electron-fixed.json b/docs/audits/terminal-completed-spawn-inputs/mapped-electron-fixed.json new file mode 100644 index 00000000000..653e5d8cd75 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/mapped-electron-fixed.json @@ -0,0 +1,66 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "fixed": true, + "sourceMode": "mapped modules with working-tree dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f", + "src/main/daemon/terminal-host-session-create.ts": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309", + "src/main/daemon/terminal-host.ts": "f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f" + }, + "reports": [ + { + "case": "completed-inputs", + "whileLive": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "liveSessionCountAtCollection": 3 + }, + { + "case": "pending-inputs", + "duringSpawn": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterPublication": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + } + }, + { + "case": "exit-and-recreate", + "exitVerdict": "exited", + "exitReason": "pty_exit_7", + "reaped": ["claimed", "claimed"], + "newIncarnation": true, + "newGeneration": true + }, + { + "case": "foreground-confirmation", + "confirmations": 1, + "preservedReceiver": true, + "owner": "shell", + "queuedPromptReleased": true + } + ] +} diff --git a/docs/audits/terminal-completed-spawn-inputs/mapped-node-baseline.json b/docs/audits/terminal-completed-spawn-inputs/mapped-node-baseline.json new file mode 100644 index 00000000000..b77daa28825 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/mapped-node-baseline.json @@ -0,0 +1,66 @@ +{ + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "fixed": false, + "sourceMode": "mapped modules with working-tree dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "src/main/daemon/terminal-host-session-create.ts": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "src/main/daemon/terminal-host.ts": "5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca" + }, + "reports": [ + { + "case": "completed-inputs", + "whileLive": { + "options": 3, + "env": 3, + "history": 3, + "signal": 3 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "liveSessionCountAtCollection": 3 + }, + { + "case": "pending-inputs", + "duringSpawn": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterPublication": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + } + }, + { + "case": "exit-and-recreate", + "exitVerdict": "exited", + "exitReason": "pty_exit_7", + "reaped": ["claimed", "claimed"], + "newIncarnation": true, + "newGeneration": true + }, + { + "case": "foreground-confirmation", + "confirmations": 1, + "preservedReceiver": true, + "owner": "shell", + "queuedPromptReleased": true + } + ] +} diff --git a/docs/audits/terminal-completed-spawn-inputs/mapped-node-fixed.json b/docs/audits/terminal-completed-spawn-inputs/mapped-node-fixed.json new file mode 100644 index 00000000000..571ae217572 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/mapped-node-fixed.json @@ -0,0 +1,66 @@ +{ + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "fixed": true, + "sourceMode": "mapped modules with working-tree dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f", + "src/main/daemon/terminal-host-session-create.ts": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309", + "src/main/daemon/terminal-host.ts": "f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f" + }, + "reports": [ + { + "case": "completed-inputs", + "whileLive": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "liveSessionCountAtCollection": 3 + }, + { + "case": "pending-inputs", + "duringSpawn": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterPublication": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + } + }, + { + "case": "exit-and-recreate", + "exitVerdict": "exited", + "exitReason": "pty_exit_7", + "reaped": ["claimed", "claimed"], + "newIncarnation": true, + "newGeneration": true + }, + { + "case": "foreground-confirmation", + "confirmations": 1, + "preservedReceiver": true, + "owner": "shell", + "queuedPromptReleased": true + } + ] +} diff --git a/docs/audits/terminal-completed-spawn-inputs/node-baseline.json b/docs/audits/terminal-completed-spawn-inputs/node-baseline.json new file mode 100644 index 00000000000..4a7048adb29 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/node-baseline.json @@ -0,0 +1,66 @@ +{ + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "fixed": false, + "sourceMode": "working-tree modules and dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "src/main/daemon/terminal-host-session-create.ts": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "src/main/daemon/terminal-host.ts": "8cd6804b249ffb0d82da7f5a7ec8faa66514b0d6e3e36472cd81e6409c3edbc4" + }, + "reports": [ + { + "case": "completed-inputs", + "whileLive": { + "options": 3, + "env": 3, + "history": 3, + "signal": 3 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "liveSessionCountAtCollection": 3 + }, + { + "case": "pending-inputs", + "duringSpawn": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterPublication": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + } + }, + { + "case": "exit-and-recreate", + "exitVerdict": "exited", + "exitReason": "pty_exit_7", + "reaped": ["claimed", "claimed"], + "newIncarnation": true, + "newGeneration": true + }, + { + "case": "foreground-confirmation", + "confirmations": 1, + "preservedReceiver": true, + "owner": "shell", + "queuedPromptReleased": true + } + ] +} diff --git a/docs/audits/terminal-completed-spawn-inputs/node-fixed.json b/docs/audits/terminal-completed-spawn-inputs/node-fixed.json new file mode 100644 index 00000000000..b2cf00a4556 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/node-fixed.json @@ -0,0 +1,66 @@ +{ + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "fixed": true, + "sourceMode": "working-tree modules and dependencies", + "sourceHashes": { + "src/main/daemon/session-output-pipeline.ts": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f", + "src/main/daemon/terminal-host-session-create.ts": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309", + "src/main/daemon/terminal-host.ts": "23cfd9c6317db30edd48a0c8bd7c54658206654703461cf12295274b23fb8844" + }, + "reports": [ + { + "case": "completed-inputs", + "whileLive": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "liveSessionCountAtCollection": 3 + }, + { + "case": "pending-inputs", + "duringSpawn": { + "options": 1, + "env": 1, + "history": 1, + "signal": 1 + }, + "afterPublication": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + }, + "afterDispose": { + "options": 0, + "env": 0, + "history": 0, + "signal": 0 + } + }, + { + "case": "exit-and-recreate", + "exitVerdict": "exited", + "exitReason": "pty_exit_7", + "reaped": ["claimed", "claimed"], + "newIncarnation": true, + "newGeneration": true + }, + { + "case": "foreground-confirmation", + "confirmations": 1, + "preservedReceiver": true, + "owner": "shell", + "queuedPromptReleased": true + } + ] +} diff --git a/docs/audits/terminal-completed-spawn-inputs/reproduce.cjs b/docs/audits/terminal-completed-spawn-inputs/reproduce.cjs new file mode 100644 index 00000000000..4900a3f4b5a --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/reproduce.cjs @@ -0,0 +1,208 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { load, evaluatedSourceHashes, sourceMode, reportPrefix } = require('./spawn-source.cjs') +const { + subprocess, + streamClient, + startWithInputs, + counts, + expected, + collect +} = require('./spawn-fixture.cjs') +const fixed = !process.argv.includes('--baseline') +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function') + +async function completedInputs(Host) { + const host = new Host({ spawnSubprocess: async () => subprocess() }) + const refs = [] + try { + for (let index = 0; index < 3; index += 1) { + const created = startWithInputs(host, `retention-${index}`) + assert.equal((await created.creation).historySeeded, true) + refs.push(created.refs) + } + await collect() + const whileLive = counts(refs) + assert.deepEqual(whileLive, expected(fixed ? 0 : 3)) + assert.equal(host.listSessions().length, 3) + assert.ok(host.getSnapshot('retention-0').snapshotAnsi.includes('retention-seed')) + await host.dispose() + await collect() + const afterDispose = counts(refs) + assert.deepEqual(afterDispose, expected(0)) + return { case: 'completed-inputs', whileLive, afterDispose, liveSessionCountAtCollection: 3 } + } finally { + await host.dispose() + } +} + +async function pendingInputs(Host) { + const gate = Promise.withResolvers() + const host = new Host({ + spawnSubprocess: async () => { + await gate.promise + return subprocess() + } + }) + const created = startWithInputs(host, 'pending') + try { + await collect() + const duringSpawn = counts([created.refs]) + assert.deepEqual(duringSpawn, expected(1)) + gate.resolve() + assert.equal((await created.creation).isNew, true) + await collect() + const afterPublication = counts([created.refs]) + assert.deepEqual(afterPublication, expected(fixed ? 0 : 1)) + await host.dispose() + await collect() + assert.deepEqual(counts([created.refs]), expected(0)) + return { + case: 'pending-inputs', + duringSpawn, + afterPublication, + afterDispose: counts([created.refs]) + } + } finally { + gate.resolve() + await created.creation + await host.dispose() + } +} + +async function exitAndRecreate(Host) { + const handles = [] + const reaped = [] + const host = new Host({ + spawnSubprocess: async () => { + const handle = subprocess() + handles.push(handle) + return handle + }, + onSessionReaped: (id) => reaped.push(id) + }) + const options = { + sessionId: 'claimed', + cols: 80, + rows: 24, + streamClient, + agentSessionEnsure: { + claim: { + digestVersion: 1, + keyId: 'key', + identityDigest: 'a'.repeat(43), + worktreeScopeDigest: 'b'.repeat(43), + agent: 'codex' + }, + surface: { + worktreeId: 'worktree', + tabId: 'tab', + leafId: '11111111-1111-4111-8111-111111111111', + terminalHandle: 'term_claimed' + } + } + } + try { + const first = await host.createOrAttach(options) + handles[0].emitExit(7) + assert.deepEqual(reaped, ['claimed']) + assert.deepEqual(host.listSessions(), []) + const evidence = ( + await host.inspectProcess('claimed', { expectedIncarnationId: first.incarnationId }) + ).foregroundProcessEvidence + assert.equal(evidence.verdict, 'exited') + assert.equal(evidence.reason, 'pty_exit_7') + assert.equal(evidence.ptyIncarnationId, first.incarnationId) + const second = await host.createOrAttach(options) + assert.equal(second.agentSessionEnsure.disposition, 'created') + assert.notEqual( + second.agentSessionEnsure.owner.generation, + first.agentSessionEnsure.owner.generation + ) + assert.notEqual(second.incarnationId, first.incarnationId) + assert.equal(handles.length, 2) + await host.dispose() + assert.deepEqual(reaped, ['claimed', 'claimed']) + return { + case: 'exit-and-recreate', + exitVerdict: evidence.verdict, + exitReason: evidence.reason, + reaped, + newIncarnation: true, + newGeneration: true + } + } finally { + await host.dispose() + } +} + +async function foregroundConfirmation(Host) { + const gate = Promise.withResolvers() + let confirmations = 0 + const handle = { + ...subprocess(), + confirmShellForeground() { + assert.equal(this, handle) + confirmations += 1 + return gate.promise + } + } + const host = new Host({ spawnSubprocess: async () => handle }) + try { + await host.createOrAttach({ sessionId: 'recovery', cols: 80, rows: 24, streamClient }) + handle.emitData('\x1b[?1049hTUI\x1b]133;D;137\x07SHELL-PROMPT') + await new Promise((resolve) => setImmediate(resolve)) + assert.equal(confirmations, 1) + gate.resolve(true) + const snapshot = await host.getSettledSnapshot('recovery') + assert.equal(snapshot.terminalOwner, 'shell') + assert.ok(snapshot.snapshotAnsi.includes('SHELL-PROMPT')) + return { + case: 'foreground-confirmation', + confirmations, + preservedReceiver: true, + owner: snapshot.terminalOwner, + queuedPromptReleased: true + } + } finally { + gate.resolve(false) + await host.dispose() + } +} + +async function main() { + const Host = await load(fixed) + const reports = [ + await completedInputs(Host), + await pendingInputs(Host), + await exitAndRecreate(Host), + await foregroundConfirmation(Host) + ] + const report = { + node: process.version, + electron: process.versions.electron ?? null, + v8: process.versions.v8, + fixed, + sourceMode, + sourceHashes: Object.fromEntries( + Object.entries(evaluatedSourceHashes).map(([file, hashes]) => [ + file, + fixed ? hashes.fixed : hashes.baseline + ]) + ), + reports + } + const file = `${reportPrefix}${process.versions.electron ? 'electron' : 'node'}-${fixed ? 'fixed' : 'baseline'}.json` + fs.writeFileSync(path.join(__dirname, file), `${JSON.stringify(report, null, 2)}\n`) + console.log(JSON.stringify(report, null, 2)) +} +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) +setTimeout(() => { + console.error('fixture timeout') + process.exit(2) +}, 15000).unref() diff --git a/docs/audits/terminal-completed-spawn-inputs/source-versions.json b/docs/audits/terminal-completed-spawn-inputs/source-versions.json new file mode 100644 index 00000000000..bdfb76e1493 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/source-versions.json @@ -0,0 +1,113 @@ +{ + "baselineCommit": "9e2c137548bf99f91255ab4862c01145e42a0883", + "sources": [ + { + "sourcePath": "src/main/daemon/session-output-pipeline.ts", + "baselineSha256": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "fixedSha256": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f" + }, + { + "sourcePath": "src/main/daemon/terminal-host-session-create.ts", + "baselineSha256": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "fixedSha256": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309" + }, + { + "sourcePath": "src/main/daemon/terminal-host.ts", + "baselineSha256": "8cd6804b249ffb0d82da7f5a7ec8faa66514b0d6e3e36472cd81e6409c3edbc4", + "fixedSha256": "23cfd9c6317db30edd48a0c8bd7c54658206654703461cf12295274b23fb8844", + "alternatePairs": [ + { + "name": "independent-main-publication", + "baselineSha256": "5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca", + "fixedSha256": "f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f" + } + ] + } + ], + "comparedRefs": [ + { + "ref": "origin/main", + "commit": "77cd61df396f25ec91ee2d5ddcbd1f55aa94f818", + "sources": [ + { + "sourcePath": "src/main/daemon/session-output-pipeline.ts", + "sha256": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "matchesBaseline": true, + "patchApplies": true, + "overlaySha256": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f", + "matchesFixed": true + }, + { + "sourcePath": "src/main/daemon/terminal-host-session-create.ts", + "sha256": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "matchesBaseline": true, + "patchApplies": true, + "overlaySha256": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309", + "matchesFixed": true + }, + { + "sourcePath": "src/main/daemon/terminal-host.ts", + "sha256": "5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca", + "matchesBaseline": false, + "patchApplies": true, + "overlaySha256": "f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f", + "matchesFixed": false + } + ] + }, + { + "ref": "v1.4.198", + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sources": [ + { + "sourcePath": "src/main/daemon/session-output-pipeline.ts", + "sha256": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "matchesBaseline": true, + "patchApplies": true, + "overlaySha256": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f", + "matchesFixed": true + }, + { + "sourcePath": "src/main/daemon/terminal-host-session-create.ts", + "sha256": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "matchesBaseline": true, + "patchApplies": true, + "overlaySha256": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309", + "matchesFixed": true + }, + { + "sourcePath": "src/main/daemon/terminal-host.ts", + "sha256": "5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca", + "matchesBaseline": false, + "patchApplies": true, + "overlaySha256": "f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f", + "matchesFixed": false + } + ] + } + ], + "publicationMain": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sources": [ + { + "sourcePath": "src/main/daemon/session-output-pipeline.ts", + "baselineSha256": "99ff9d936469a59e07f979b19344815c8636cd58a985e139614f221e99eaf8c9", + "fixedSha256": "94f6f221d85388274d585ad714fda25c32a824942950074e4e95b85c94a6771f", + "patchApplies": true + }, + { + "sourcePath": "src/main/daemon/terminal-host-session-create.ts", + "baselineSha256": "44fb82244a3307b5c89ebf724640be585dfe1b79566d11649b38eaf518c54d32", + "fixedSha256": "d54cbaff13c8daba1766e4fec24a6903ecdf1486249f1a9e5d96a207de410309", + "patchApplies": true + }, + { + "sourcePath": "src/main/daemon/terminal-host.ts", + "baselineSha256": "5216562b3c10c5fce4238614dc3a81887c970359a60e7e23179c6dfa15ccbcca", + "fixedSha256": "f22856504559a6bb78498c6d5aae07cbbd80a21019723278a560628e0142236f", + "patchApplies": true + } + ], + "dependencyScope": "Only these three modules are mapped; other dependencies are current worktree source." + } +} diff --git a/docs/audits/terminal-completed-spawn-inputs/spawn-fixture.cjs b/docs/audits/terminal-completed-spawn-inputs/spawn-fixture.cjs new file mode 100644 index 00000000000..193ab304a33 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/spawn-fixture.cjs @@ -0,0 +1,81 @@ +const assert = require('node:assert/strict') + +function subprocess() { + let dataListener + let exitListener + return { + pid: 424242, + getForegroundProcess: () => null, + write() {}, + resize() {}, + signal() {}, + kill() { + exitListener?.(0) + }, + forceKill() { + exitListener?.(137) + }, + terminateOwnedTree: () => 'unavailable', + onData(listener) { + dataListener = listener + }, + onExit(listener) { + exitListener = listener + }, + dispose() { + dataListener = undefined + exitListener = undefined + }, + emitData(data) { + dataListener?.(data) + }, + emitExit(code) { + exitListener?.(code) + } + } +} + +// These callbacks must not share a lexical context with the request's signal. +const streamClient = { onData() {}, onExit() {} } +function startWithInputs(host, sessionId) { + const controller = new AbortController() + const env = { RETENTION_FIXTURE: 'x'.repeat(1024) } + const historySeedChunks = ['retention-seed\r\n'] + const options = { + sessionId, + cols: 80, + rows: 24, + env, + historySeedChunks, + streamClient, + cancelSignal: controller.signal, + isCanceled: () => controller.signal.aborted + } + return { + refs: { + options: new WeakRef(options), + env: new WeakRef(env), + history: new WeakRef(historySeedChunks), + signal: new WeakRef(controller.signal) + }, + creation: host.createOrAttach(options) + } +} + +function counts(refs) { + return Object.fromEntries( + ['options', 'env', 'history', 'signal'].map((key) => [ + key, + refs.filter((ref) => ref[key].deref() !== undefined).length + ]) + ) +} +const expected = (count) => ({ options: count, env: count, history: count, signal: count }) +async function collect() { + assert.equal(typeof global.gc, 'function') + for (let round = 0; round < 4; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + global.gc() + } +} +module.exports = { subprocess, streamClient, startWithInputs, counts, expected, collect } diff --git a/docs/audits/terminal-completed-spawn-inputs/spawn-source.cjs b/docs/audits/terminal-completed-spawn-inputs/spawn-source.cjs new file mode 100644 index 00000000000..5001b5d4d31 --- /dev/null +++ b/docs/audits/terminal-completed-spawn-inputs/spawn-source.cjs @@ -0,0 +1,114 @@ +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 versions = require('./source-versions.json') + +const root = path.resolve(__dirname, '../../..') +const readText = (file) => readFileSync(file, 'utf8').replace(/\r\n/g, '\n') +const sha = (value) => createHash('sha256').update(value).digest('hex') +const patches = parsePatch(readText(path.join(__dirname, 'fix.patch'))) +assert.equal(patches.length, versions.sources.length) +const fixedSources = new Map() +const baselineSources = new Map() +const evaluatedSourceHashes = {} +const sourceMapPath = process.env.ORCA_SPAWN_INPUT_PROOF_SOURCE_MAP +const sourceOverrides = sourceMapPath ? JSON.parse(readText(path.resolve(sourceMapPath))) : null +if (sourceMapPath) { + assert.equal(typeof sourceOverrides, 'object') + assert.notEqual(sourceOverrides, null) + assert.equal(Array.isArray(sourceOverrides), false) + assert.deepEqual( + Object.keys(sourceOverrides).sort(), + versions.sources.map((source) => source.sourcePath).sort() + ) +} +for (const source of versions.sources) { + const file = path.join(root, source.sourcePath) + const fixed = sourceOverrides ? sourceOverrides[source.sourcePath] : readText(file) + assert.equal(typeof fixed, 'string') + const pair = [source, ...(source.alternatePairs ?? [])].find( + (entry) => entry.fixedSha256 === sha(fixed) + ) + assert.ok(pair, `Unreviewed product source: ${source.sourcePath}`) + const patch = patches.find((entry) => entry.oldFileName === `a/${source.sourcePath}`) + assert.ok(patch) + const baseline = applyPatch(fixed, reversePatch(patch)) + assert.notEqual(baseline, false) + assert.equal(sha(baseline), pair.baselineSha256, `Baseline changed: ${source.sourcePath}`) + fixedSources.set(file, fixed) + baselineSources.set(file, baseline) + evaluatedSourceHashes[source.sourcePath] = { baseline: sha(baseline), fixed: sha(fixed) } +} + +const sourceMode = sourceMapPath + ? 'mapped modules with working-tree dependencies' + : 'working-tree modules and dependencies' +const reportPrefix = sourceMapPath ? 'mapped-' : '' + +async function loadExports(fixed) { + const sources = fixed ? fixedSources : baselineSources + const build = await esbuild.build({ + stdin: { + contents: [ + "export { TerminalHost } from './src/main/daemon/terminal-host'", + "export { DaemonTerminalAdmission } from './src/main/daemon/daemon-terminal-admission'", + "export { DaemonPtySpawnPreparations } from './src/main/daemon/daemon-pty-spawn-preparations'" + ].join(';'), + resolveDir: root, + loader: 'ts' + }, + platform: 'node', + format: 'cjs', + bundle: true, + packages: 'external', + write: false, + plugins: [ + { + name: 'reviewed-spawn-input-sources', + setup(builder) { + builder.onLoad( + { filter: /(?:terminal-host(?:-session-create)?|session-output-pipeline)\.ts$/ }, + (args) => { + const contents = sources.get(args.path) + return contents === undefined ? undefined : { contents, loader: 'ts' } + } + ) + builder.onResolve({ filter: /pty-descendant-termination$/ }, () => ({ + path: 'no-os-signals', + namespace: 'fixture' + })) + builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ + contents: + "export function killWithDescendantSweep() { throw new Error('Unexpected real process teardown') }", + loader: 'js' + })) + } + } + ] + }) + const filename = path.join(__dirname, 'bundled-terminal-host.cjs') + const loaded = new Module(filename, module) + loaded.filename = filename + loaded.paths = Module._nodeModulePaths(__dirname) + loaded._compile(build.outputFiles[0].text, filename) + return loaded.exports +} + +async function load(fixed) { + return (await loadExports(fixed)).TerminalHost +} + +module.exports = { + load, + loadExports, + versions, + sha, + baselineSources, + evaluatedSourceHashes, + sourceMode, + reportPrefix +} diff --git a/src/main/daemon/session-output-pipeline.ts b/src/main/daemon/session-output-pipeline.ts index c249e4d1d31..f67d2e15b92 100644 --- a/src/main/daemon/session-output-pipeline.ts +++ b/src/main/daemon/session-output-pipeline.ts @@ -14,6 +14,7 @@ export function createSessionOutputPipeline(opts: { subprocess: SubprocessHandle isAlive: () => boolean }): { output: SessionOutputPlane; recoveryBarrier: TerminalShellRecoveryBarrier } { + const { subprocess, isAlive } = opts let barrier: TerminalShellRecoveryBarrier | null = null const output = new SessionOutputPlane({ cols: opts.cols, @@ -24,9 +25,9 @@ export function createSessionOutputPipeline(opts: { getTerminalOwner: () => barrier?.getOwner() }) const recoveryBarrier = new TerminalShellRecoveryBarrier({ - confirmShellForeground: async () => (await opts.subprocess.confirmShellForeground?.()) ?? false, + confirmShellForeground: async () => (await subprocess.confirmShellForeground?.()) ?? false, release: (emission) => output.emit(emission), - isAlive: opts.isAlive + isAlive }) barrier = recoveryBarrier return { output, recoveryBarrier } diff --git a/src/main/daemon/terminal-host-session-create.ts b/src/main/daemon/terminal-host-session-create.ts index 8f6833c3d9f..fc4cc01088a 100644 --- a/src/main/daemon/terminal-host-session-create.ts +++ b/src/main/daemon/terminal-host-session-create.ts @@ -150,7 +150,11 @@ async function spawnAndPublishSession( historySeedChunks: opts.historySeedChunks, ...(opts.startupIngress ? { startupIngress: opts.startupIngress } : {}), wslDistro, - onExit: () => deps.onSessionExit(opts.sessionId, opts.agentSessionGeneration), + onExit: createSessionExitHandler( + deps.onSessionExit, + opts.sessionId, + opts.agentSessionGeneration + ), ...(deps.reportReadinessEvent ? { reportReadinessEvent: deps.reportReadinessEvent } : {}), ...(opts.shellReadyTimeoutMs !== undefined ? { shellReadyTimeoutMs: opts.shellReadyTimeoutMs } @@ -212,6 +216,14 @@ async function spawnAndPublishSession( } } +function createSessionExitHandler( + onSessionExit: TerminalHostSessionCreateDependencies['onSessionExit'], + sessionId: string, + generation: string | undefined +): () => void { + return () => onSessionExit(sessionId, generation) +} + // Why R_OK|X_OK: listing a directory needs read, and entering it needs search — both are what // TCC withholds. A non-permission failure (ENOENT, ENOTDIR) reads as readable so it can never // masquerade as a permission denial. diff --git a/src/main/daemon/terminal-host-spawn-input-retention.test.ts b/src/main/daemon/terminal-host-spawn-input-retention.test.ts new file mode 100644 index 00000000000..651fddcb0bf --- /dev/null +++ b/src/main/daemon/terminal-host-spawn-input-retention.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, it, vi } from 'vitest' +import type { SubprocessHandle } from './session-subprocess-handle' +import type { InternalCreateOrAttachOptions } from './terminal-host-agent-session-claim' +import { TerminalHost } from './terminal-host' + +vi.mock('../pty-descendant-termination', () => ({ + killWithDescendantSweep: () => { + throw new Error('The retention fixture must not signal real processes') + } +})) + +function subprocess() { + let dataListener: ((data: string) => void) | undefined + let exitListener: ((code: number) => void) | undefined + return { + pid: 424242, + getForegroundProcess: () => null, + write() {}, + resize() {}, + signal() {}, + kill() { + exitListener?.(0) + }, + forceKill() { + exitListener?.(137) + }, + terminateOwnedTree: () => 'unavailable' as const, + onData(listener: (data: string) => void) { + dataListener = listener + }, + onExit(listener: (code: number) => void) { + exitListener = listener + }, + dispose() { + dataListener = undefined + exitListener = undefined + }, + emitData(data: string) { + dataListener?.(data) + }, + emitExit(code: number) { + exitListener?.(code) + } + } satisfies SubprocessHandle & { + emitData: (data: string) => void + emitExit: (code: number) => void + } +} + +const streamClient = { onData() {}, onExit() {} } + +function startWithInputs(host: TerminalHost, sessionId: string) { + const controller = new AbortController() + const env = { RETENTION_FIXTURE: 'x'.repeat(1024) } + const historySeedChunks = ['retention-seed\r\n'] + const options: InternalCreateOrAttachOptions = { + sessionId, + cols: 80, + rows: 24, + env, + historySeedChunks, + streamClient, + cancelSignal: controller.signal, + isCanceled: () => controller.signal.aborted + } + return { + refs: [ + new WeakRef(options), + new WeakRef(env), + new WeakRef(historySeedChunks), + new WeakRef(controller.signal) + ], + creation: host.createOrAttach(options) + } +} + +async function collect(): Promise { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + for (let round = 0; round < 4; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } +} + +describe('TerminalHost completed spawn inputs', () => { + it('releases request, environment, consumed history and cancellation inputs for live sessions', async () => { + const host = new TerminalHost({ spawnSubprocess: async () => subprocess() }) + try { + const refs: WeakRef[] = [] + for (let index = 0; index < 3; index += 1) { + const created = startWithInputs(host, `retention-${index}`) + expect((await created.creation).historySeeded).toBe(true) + refs.push(...created.refs) + } + await collect() + expect(refs.map((ref) => ref.deref() === undefined)).toEqual(Array(12).fill(true)) + expect(host.listSessions()).toHaveLength(3) + expect(host.getSnapshot('retention-0')?.snapshotAnsi).toContain('retention-seed') + } finally { + await host.dispose() + } + }) + + it('retains inputs during spawn and releases them after publication', async () => { + const gate = Promise.withResolvers() + const host = new TerminalHost({ + spawnSubprocess: async () => { + await gate.promise + return subprocess() + } + }) + const created = startWithInputs(host, 'pending') + try { + await collect() + expect(created.refs.map((ref) => ref.deref() !== undefined)).toEqual(Array(4).fill(true)) + gate.resolve() + expect((await created.creation).isNew).toBe(true) + await collect() + expect(created.refs.map((ref) => ref.deref() === undefined)).toEqual(Array(4).fill(true)) + expect(host.listSessions()).toHaveLength(1) + } finally { + gate.resolve() + await created.creation + await host.dispose() + } + }) + + it('reaps exited sessions, preserves exit evidence and releases claimed generations', async () => { + const handles: ReturnType[] = [] + const reaped: string[] = [] + const host = new TerminalHost({ + spawnSubprocess: async () => { + const handle = subprocess() + handles.push(handle) + return handle + }, + onSessionReaped: (sessionId) => reaped.push(sessionId) + }) + const options = { + sessionId: 'claimed', + cols: 80, + rows: 24, + streamClient, + agentSessionEnsure: { + claim: { + digestVersion: 1 as const, + keyId: 'key', + identityDigest: 'a'.repeat(43), + worktreeScopeDigest: 'b'.repeat(43), + agent: 'codex' as const + }, + surface: { + worktreeId: 'worktree', + tabId: 'tab', + leafId: '11111111-1111-4111-8111-111111111111', + terminalHandle: 'term_claimed' + } + } + } + try { + const first = await host.createOrAttach(options) + handles[0]?.emitExit(7) + expect(reaped).toEqual(['claimed']) + expect(host.listSessions()).toEqual([]) + expect( + await host.inspectProcess('claimed', { expectedIncarnationId: first.incarnationId }) + ).toMatchObject({ + foregroundProcessEvidence: { + verdict: 'exited', + reason: 'pty_exit_7', + ptyIncarnationId: first.incarnationId + } + }) + const second = await host.createOrAttach(options) + expect(second.agentSessionEnsure?.disposition).toBe('created') + expect(second.incarnationId).not.toBe(first.incarnationId) + expect(second.agentSessionEnsure?.owner.generation).not.toBe( + first.agentSessionEnsure?.owner.generation + ) + expect(handles).toHaveLength(2) + } finally { + await host.dispose() + } + expect(reaped).toEqual(['claimed', 'claimed']) + }) + + it('confirms shell recovery with the subprocess receiver and releases queued output', async () => { + let confirmations = 0 + const gate = Promise.withResolvers() + const handle = { + ...subprocess(), + confirmShellForeground() { + expect(this).toBe(handle) + confirmations += 1 + return gate.promise + } + } + const host = new TerminalHost({ spawnSubprocess: async () => handle }) + try { + await host.createOrAttach({ sessionId: 'recovery', cols: 80, rows: 24, streamClient }) + handle.emitData('\x1b[?1049hTUI\x1b]133;D;137\x07SHELL-PROMPT') + await vi.waitFor(() => expect(confirmations).toBe(1)) + gate.resolve(true) + const snapshot = await host.getSettledSnapshot('recovery') + expect(snapshot?.terminalOwner).toBe('shell') + expect(snapshot?.snapshotAnsi).toContain('SHELL-PROMPT') + } finally { + gate.resolve(false) + await host.dispose() + } + }) +}) diff --git a/src/main/daemon/terminal-host.ts b/src/main/daemon/terminal-host.ts index 95bedd1a7fd..9c164354564 100644 --- a/src/main/daemon/terminal-host.ts +++ b/src/main/daemon/terminal-host.ts @@ -54,7 +54,6 @@ export class TerminalHost { private onSessionReaped: TerminalHostOptions['onSessionReaped'] private reportReadinessEvent: TerminalHostOptions['reportReadinessEvent'] private onFinalCheckpoint: TerminalHostOptions['onFinalCheckpoint'] - private maxTombstones: number private creationFenced = false private disposePromise: Promise | null = null private readonly agentSessionOwners = new ClaimedAgentPtyOwnerRegistry() @@ -71,8 +70,7 @@ export class TerminalHost { this.onSessionReaped = opts.onSessionReaped this.reportReadinessEvent = opts.reportReadinessEvent this.onFinalCheckpoint = opts.onFinalCheckpoint - this.maxTombstones = opts.maxTombstones ?? DEFAULT_MAX_TOMBSTONES - this.killedTombstones = new TerminalHostTombstones(this.maxTombstones) + this.killedTombstones = new TerminalHostTombstones(opts.maxTombstones ?? DEFAULT_MAX_TOMBSTONES) } async createOrAttach(opts: InternalCreateOrAttachOptions): Promise { @@ -123,20 +121,7 @@ export class TerminalHost { ...(this.reportReadinessEvent ? { reportReadinessEvent: this.reportReadinessEvent } : {}), - onSessionExit: (sessionId, generation) => { - const session = this.sessions.get(sessionId) - if (session) { - pruneRetiredPtyIncarnations(this.retiredIncarnations) - this.retiredIncarnations.set(sessionId, { - incarnationId: session.incarnationId, - code: session.exitCode ?? 0, - expiresAt: Date.now() + REMOTE_FOREGROUND_TOMBSTONE_RETENTION_MS - }) - } - this.agentSessionOwners.release(sessionId, generation) - this.agentSessionGenerations.forget(sessionId, generation) - this.reapSession(sessionId) - } + onSessionExit: this.handleSessionExit.bind(this) }) } }) @@ -146,6 +131,21 @@ export class TerminalHost { } } + private handleSessionExit(sessionId: string, generation: string | undefined): void { + const session = this.sessions.get(sessionId) + if (session) { + pruneRetiredPtyIncarnations(this.retiredIncarnations) + this.retiredIncarnations.set(sessionId, { + incarnationId: session.incarnationId, + code: session.exitCode ?? 0, + expiresAt: Date.now() + REMOTE_FOREGROUND_TOMBSTONE_RETENTION_MS + }) + } + this.agentSessionOwners.release(sessionId, generation) + this.agentSessionGenerations.forget(sessionId, generation) + this.reapSession(sessionId) + } + private assertCreateOrAttachAllowed(opts: InternalCreateOrAttachOptions): void { if (this.creationFenced) { throw new Error('Terminal host is shutting down') From b899b225456f0744fadb02e4129a977b1e0361bc Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:16 -0700 Subject: [PATCH 15/59] fix: release native PTY spawn environment after setup (#21140) Co-authored-by: m4air --- .../native-pty-spawn-env-retention/README.md | 51 ++ .../before.config.mjs | 24 + .../electron-results.json | 533 ++++++++++++++++++ .../native-pty-spawn-env-retention/fix.patch | 17 + .../node-results.json | 532 +++++++++++++++++ .../reproduce.cjs | 71 +++ .../scenario.cjs | 148 +++++ .../source-versions.json | 59 ++ .../sources.cjs | 97 ++++ .../validation.json | 53 ++ .../pty-subprocess-env-retention.test.ts | 128 +++++ .../pty-subprocess/subprocess-handle.ts | 3 +- 12 files changed, 1715 insertions(+), 1 deletion(-) create mode 100644 docs/audits/native-pty-spawn-env-retention/README.md create mode 100644 docs/audits/native-pty-spawn-env-retention/before.config.mjs create mode 100644 docs/audits/native-pty-spawn-env-retention/electron-results.json create mode 100644 docs/audits/native-pty-spawn-env-retention/fix.patch create mode 100644 docs/audits/native-pty-spawn-env-retention/node-results.json create mode 100644 docs/audits/native-pty-spawn-env-retention/reproduce.cjs create mode 100644 docs/audits/native-pty-spawn-env-retention/scenario.cjs create mode 100644 docs/audits/native-pty-spawn-env-retention/source-versions.json create mode 100644 docs/audits/native-pty-spawn-env-retention/sources.cjs create mode 100644 docs/audits/native-pty-spawn-env-retention/validation.json create mode 100644 src/main/daemon/pty-subprocess-env-retention.test.ts diff --git a/docs/audits/native-pty-spawn-env-retention/README.md b/docs/audits/native-pty-spawn-env-retention/README.md new file mode 100644 index 00000000000..35b57f8dbb1 --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/README.md @@ -0,0 +1,51 @@ +# Native PTY spawn environment lifetime + +The native PTY handle's exit callback captured its complete creation arguments solely to read `reportsChildExitStatus`. Those arguments include the merged spawn environment. Copying that boolean before registering the callback releases the arguments and environment while the PTY remains live. + +This is per-handle retention: the original objects also collect after the handle and native event owner become unreachable. It does not establish retention after every terminal closes, native PTY memory usage, an RSS slope, or the cause of #19831. + +## Ownership and compatibility + +- `src/main/daemon/pty-subprocess.ts:72–113` creates the environment, completes preflight and native spawn, then passes a fresh object literal to `createDaemonPtySubprocessHandle`. This is the sole production call site; the caller never stores or mutates that object afterward. +- `src/main/daemon/pty-subprocess/native-pty-spawn.ts:29–74` computes `reportsChildExitStatus` synchronously from the selected native launch command. Every successful return copies the boolean into its result. It is an immutable spawn fact in this call chain. +- `src/main/daemon/pty-subprocess/subprocess-handle.ts:26–69` needs the process, projected foreground metadata, scalar exit-status fact and PATH. Its long-lived exit callback previously retained the whole argument object. The fix changes only that capture. Native spawning, native signal ownership, physical-exit ordering, disposal and output buffering are unchanged. +- The environment is a fresh merged object, but many of its string values may already be shared with `process.env`. Releasing its reachability does not imply an equivalent reduction in resident bytes. Required PATH remains reachable through `shellPathEnv`. + +`source-versions.json` records exact hashes. Main commit `291b4ddd6f1c1af480169885e0fda7f9c78ff053` exactly matches the audited wrapper baseline. Release `v1.4.198` (`e0826956fcfc532f5a1e55b5e081f2e57e553c43`) has the same environment capture and callers, but predates unrelated I/O-failure and exit-listener ordering changes. The two-line patch applies to both named sources. This is a historical source comparison, not a historical packaged-runtime reproduction. + +The change stays inside the daemon's execution-host wrapper. It adds no remote wire data or client-side process verdict, and depends on neither a git worktree nor a folder workspace. + +## Bounded before/after proof + +`sources.cjs` reverses `fix.patch` in memory and checks the exact baseline and fixed SHA-256 values before bundling either version. It imports the actual foreground tracker and pre-listener queue, and records all effective source dependency hashes and the generated bundle hash. No git refs, copied production implementation, build outputs, credentials or ignored notes are required to rerun it. + +Source and patch reads normalize CRLF to LF before reversal and hashing; recorded named-source, dependency and event-emitter hashes use canonical LF. The proof also feeds synthetic CRLF source and patch text into the loader in memory and verifies identical before/after source and hashes, without writing product files. This checks the checkout line-ending case, not a Windows runtime. + +The fixture uses the installed `node-pty` JavaScript event emitter and an inert process port. Native termination imports and `process.kill` are guarded; no native PTY, subprocess scan, OS signal, socket or window is created. WeakRefs measure one small argument object and one small environment object, with no payload amplification. The deadline is 15 seconds and the heap limit in these commands is 128 MiB. + +| Runtime | Live handle before: args / env | Live handle after: args / env | After owner drop, both versions | +| ------------------------------ | ------------------------------ | ----------------------------- | ------------------------------- | +| Node 26.6.0 | 1 / 1 | 0 / 0 | 0 / 0 | +| Electron 43.7.0 / Node 24.21.0 | 1 / 1 | 0 / 0 | 0 / 0 | + +Both versions preserve PATH, startup-delivery metadata, raw foreground lookup, pre-listener output and exit replay, normal exit codes, signal causes, unavailable wrapper status, dead-handle signal guards and idempotent disposal. `node-results.json` and `electron-results.json` contain the measured results. + +From the repository root, run the Node proof: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=128 docs/audits/native-pty-spawn-env-retention/reproduce.cjs +``` + +For Electron, run the installed Electron executable with `--expose-gc --max-old-space-size=128 docs/audits/native-pty-spawn-env-retention/reproduce.cjs`. Set `ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1` in its environment. This keeps Electron in Node mode; it creates no windows. + +## Regression checks + +The new lifetime regression measures collection before native exit, then confirms that the live handle still delivers data and exit. Two more cases preserve both exit-status interpretations after collection. Existing lifecycle, foreground identity/cadence, environment inheritance and I/O-failure cleanup suites cover neighboring contracts. + +The fixed six-file run passed 114 tests with four existing platform skips. The reversible baseline overlay ran the new and existing lifecycle suites: one expected lifetime failure, 29 passing controls. To reproduce the overlay without editing product files: + +```sh +ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/native-pty-spawn-env-retention/before.config.mjs src/main/daemon/pty-subprocess-env-retention.test.ts src/main/daemon/pty-subprocess-handle-lifecycle.test.ts +``` + +`validation.json` records the verification commands and outcomes. The pending-creation cancellation audit is separate and is not changed here. diff --git a/docs/audits/native-pty-spawn-env-retention/before.config.mjs b/docs/audits/native-pty-spawn-env-retention/before.config.mjs new file mode 100644 index 00000000000..cf47f99f81c --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/before.config.mjs @@ -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/native-pty-spawn-env-retention/sources.cjs') +) +const { before } = loadSources() +export default mergeConfig( + baseConfig, + defineConfig({ + plugins: [ + { + name: 'native-pty-env-before-fix', + enforce: 'pre', + transform(_code, id) { + const source = before.get(resolve(id.split('?')[0])) + return source === undefined ? undefined : { code: source, map: null } + } + } + ] + }) +) diff --git a/docs/audits/native-pty-spawn-env-retention/electron-results.json b/docs/audits/native-pty-spawn-env-retention/electron-results.json new file mode 100644 index 00000000000..207e8757ea2 --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/electron-results.json @@ -0,0 +1,533 @@ +{ + "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" + }, + "sourceHashLineEndings": "canonical LF", + "crlfLoaderControl": { + "syntheticCrlfReads": 2, + "identicalSourcesAndHashes": true + }, + "scope": "Actual wrapper, foreground tracker and pre-listener queue, with the installed node-pty event emitter and an inert native process. No native PTY, process scan, OS signal or window. Small object counts, no payload amplification.", + "nodePty": { + "version": "1.1.0", + "eventEmitterSha256": "f1c14613aa90c10def4ca7238329270871997eb26f919f7074dc25533e3e75dd" + }, + "reports": { + "before": { + "whileLive": { + "args": 1, + "env": 1 + }, + "afterOwnerDrop": { + "args": 0, + "env": 0 + }, + "controls": [ + "PATH retained", + "raw foreground receiver", + "pre-listener data/exit", + "status unavailable", + "signal cause", + "dead signal guard", + "idempotent dispose" + ] + }, + "after": { + "whileLive": { + "args": 0, + "env": 0 + }, + "afterOwnerDrop": { + "args": 0, + "env": 0 + }, + "controls": [ + "PATH retained", + "raw foreground receiver", + "pre-listener data/exit", + "status unavailable", + "signal cause", + "dead signal guard", + "idempotent dispose" + ] + } + }, + "versions": { + "before": { + "sourceHashes": { + "src/main/daemon/pty-subprocess/subprocess-handle.ts": { + "before": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e", + "after": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a" + } + }, + "bundleSha256": "a546917f2c969df3c010031537338121ed0de0ce6e60f45b39ab52dc776d4a7c", + "dependencies": [ + { + "path": "src/shared/pty-slave-line-discipline-echo.ts", + "sha256": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4" + }, + { + "path": "src/main/pty/node-pty-pts-name.ts", + "sha256": "aa1a50bd935895cbd1acc03fffd16ca735ede9d563cece849e7fdede99bc95a6" + }, + { + "path": "src/main/daemon/daemon-pty-size.ts", + "sha256": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741" + }, + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + }, + { + "path": "src/shared/cross-platform-path.ts", + "sha256": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3" + }, + { + "path": "src/shared/workspace-scope.ts", + "sha256": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581" + }, + { + "path": "src/shared/pty-session-id-format.ts", + "sha256": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b" + }, + { + "path": "src/shared/worktree/id.ts", + "sha256": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30" + }, + { + "path": "src/main/providers/agent-foreground-context-paths.ts", + "sha256": "df3ad7d74ff4048999231492aab490336078b915e9033cab77ab9e51b14c062f" + }, + { + "path": "src/shared/orca-cli-command-name.ts", + "sha256": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0" + }, + { + "path": "src/shared/tui-agent-config.ts", + "sha256": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad" + }, + { + "path": "src/shared/agent-node-entrypoint-identities.ts", + "sha256": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4" + }, + { + "path": "src/shared/print-mode-headless-command.ts", + "sha256": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a" + }, + { + "path": "src/shared/ante-headless-command.ts", + "sha256": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620" + }, + { + "path": "src/shared/prime-agent-headless-command.ts", + "sha256": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3" + }, + { + "path": "src/shared/agent-headless-command.ts", + "sha256": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87" + }, + { + "path": "src/shared/command-token-scanner.ts", + "sha256": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0" + }, + { + "path": "src/shared/agent-process-recognition.ts", + "sha256": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4" + }, + { + "path": "src/shared/synthetic-agent-title.ts", + "sha256": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + { + "path": "src/shared/foreground-wrapper-agent.ts", + "sha256": "87a2caa7daa616c96cf308a5413abf3eac21fe01411ff6bcc1e166d1cd22aa77" + }, + { + "path": "src/shared/process-table-snapshot.ts", + "sha256": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + }, + { + "path": "src/shared/process-table-snapshot-reader.ts", + "sha256": "7d35601a2a7bf7a9c5b4a897995e0e8a15775b4d35939f3dd1abe1a042bb1c5e" + }, + { + "path": "src/shared/process-table-index.ts", + "sha256": "30f236b7d0275f05673c0a7982bb0aa67c9319762749f5a583c5c31e51e31f36" + }, + { + "path": "src/shared/shell-process-detection.ts", + "sha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f" + }, + { + "path": "src/main/windows/windows-command-line-recovery-health.ts", + "sha256": "a135da3611d54fecb0dba52b4d81e06ca1532680424b55e7efa315b7c886ec33" + }, + { + "path": "src/shared/child-process/windows-command-line.ts", + "sha256": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + }, + { + "path": "src/shared/child-process/windows-cmd-shim-resolution.ts", + "sha256": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + }, + { + "path": "src/shared/child-process/spawn-resolution.ts", + "sha256": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + }, + { + "path": "src/shared/child-process/process-tree-kill-gate.ts", + "sha256": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + }, + { + "path": "src/shared/child-process/process-tree-termination.ts", + "sha256": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + }, + { + "path": "src/shared/child-process/bounded-output-sink.ts", + "sha256": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + }, + { + "path": "src/shared/child-process/child-termination-reporter.ts", + "sha256": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + }, + { + "path": "src/shared/child-process/process-spec.ts", + "sha256": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + }, + { + "path": "src/shared/child-process/run-process.ts", + "sha256": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + }, + { + "path": "src/shared/child-process/windows-system-binary.ts", + "sha256": "0c42c7c454534a0937f4153e9708aeb243bd930d130ffc6493ac42d21405606e" + }, + { + "path": "src/main/windows/windows-process-table-cim-scan.ts", + "sha256": "ecfa487a3139cc087eae78ff61bf358f7eb62aec3b20e9d064d96b6e37862ab0" + }, + { + "path": "src/main/windows/windows-process-table.ts", + "sha256": "de0565f724f49ca393bb55c1618e697ea34e98bfee132c196d0c3e97713aabe1" + }, + { + "path": "src/main/providers/windows-foreground-process-rows.ts", + "sha256": "374accddf905f5b60fb1be4010e8160bfaf7f1a3834ecaffbf0119e555c7a765" + }, + { + "path": "src/main/providers/windows-agent-foreground-process.ts", + "sha256": "a7056960617a9a5c740209a3a96280c4e57da69b7604e17f148c0c2195d0e5e9" + }, + { + "path": "src/shared/foreground-process-selection.ts", + "sha256": "18005d15f552cb8805148cebb330636f6cec0f8d98d2eca83444aef7794d3767" + }, + { + "path": "src/main/providers/agent-foreground-process-remote-evidence.ts", + "sha256": "c46feb5f19d873bbf08ff607a6a86a53a9d982ece7aa43e471aaad640621ad4d" + }, + { + "path": "src/main/providers/agent-foreground-process-batch.ts", + "sha256": "6f8c785ddd701883f7b44231b152a0c32b8a5a3db24529347085c1ac43127ce6" + }, + { + "path": "src/main/providers/agent-foreground-process.ts", + "sha256": "1f76c512e3c308959f11665c749e9d001da03661ad5bc2230a84125bc4aa8368" + }, + { + "path": "src/main/providers/windows-pty-job-membership.ts", + "sha256": "4b5031a7cbac09c967c4269a247311e09d6b2617a630dd1743bf0a9ddac37c5f" + }, + { + "path": "src/main/daemon/pty-subprocess/pty-shell-foreground-confirmation.ts", + "sha256": "aea01ded6b790b26a06314499eaa1db993c86660a076a710a73aa06a6ba1faee" + }, + { + "path": "src/main/providers/windows-cached-agent-revalidation.ts", + "sha256": "b7e60bf9e939b069488640b291bd025386cfa413b6431edd61563dba30bcc6be" + }, + { + "path": "src/main/providers/windows-console-attached-processes.ts", + "sha256": "63de2e44605c28139204c196cc1add202c723f4896c6f36757e92ab85a7fe065" + }, + { + "path": "src/main/daemon/pty-subprocess/foreground-fallback-process.ts", + "sha256": "a2a68908fa5f59a940d8b280073f5737374cb5f6d9938c64be2a4d26433d7827" + }, + { + "path": "src/main/daemon/pty-session-id.ts", + "sha256": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878" + }, + { + "path": "src/main/daemon/pty-subprocess/foreground-process-tracker.ts", + "sha256": "c29a04d412294f2ef3b46ec2fe7fd8f680ba00aadf11db4f9faea01b1b8c8df3" + }, + { + "path": "src/shared/terminal-exit-cause.ts", + "sha256": "cdbe03be86d26c6489a257e886cb6ca6b2301dc1a82752d6a028d66de46903a2" + }, + { + "path": "src/main/daemon/pty-subprocess/pre-listener-events.ts", + "sha256": "32b290969b16c2676bbc9801cf563fc1988f16131678ce3cf0c627ffeaf401a2" + }, + { + "path": "src/main/daemon/pty-subprocess/subprocess-handle.ts", + "sha256": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e" + } + ] + }, + "after": { + "sourceHashes": { + "src/main/daemon/pty-subprocess/subprocess-handle.ts": { + "before": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e", + "after": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a" + } + }, + "bundleSha256": "54e4ef70dd3262f94b679289440b882dfedacfb5e0af325d51d1ec2a2b455929", + "dependencies": [ + { + "path": "src/shared/pty-slave-line-discipline-echo.ts", + "sha256": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4" + }, + { + "path": "src/main/pty/node-pty-pts-name.ts", + "sha256": "aa1a50bd935895cbd1acc03fffd16ca735ede9d563cece849e7fdede99bc95a6" + }, + { + "path": "src/main/daemon/daemon-pty-size.ts", + "sha256": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741" + }, + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + }, + { + "path": "src/shared/cross-platform-path.ts", + "sha256": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3" + }, + { + "path": "src/shared/workspace-scope.ts", + "sha256": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581" + }, + { + "path": "src/shared/pty-session-id-format.ts", + "sha256": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b" + }, + { + "path": "src/shared/worktree/id.ts", + "sha256": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30" + }, + { + "path": "src/main/providers/agent-foreground-context-paths.ts", + "sha256": "df3ad7d74ff4048999231492aab490336078b915e9033cab77ab9e51b14c062f" + }, + { + "path": "src/shared/orca-cli-command-name.ts", + "sha256": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0" + }, + { + "path": "src/shared/tui-agent-config.ts", + "sha256": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad" + }, + { + "path": "src/shared/agent-node-entrypoint-identities.ts", + "sha256": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4" + }, + { + "path": "src/shared/print-mode-headless-command.ts", + "sha256": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a" + }, + { + "path": "src/shared/ante-headless-command.ts", + "sha256": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620" + }, + { + "path": "src/shared/prime-agent-headless-command.ts", + "sha256": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3" + }, + { + "path": "src/shared/agent-headless-command.ts", + "sha256": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87" + }, + { + "path": "src/shared/command-token-scanner.ts", + "sha256": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0" + }, + { + "path": "src/shared/agent-process-recognition.ts", + "sha256": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4" + }, + { + "path": "src/shared/synthetic-agent-title.ts", + "sha256": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + { + "path": "src/shared/foreground-wrapper-agent.ts", + "sha256": "87a2caa7daa616c96cf308a5413abf3eac21fe01411ff6bcc1e166d1cd22aa77" + }, + { + "path": "src/shared/process-table-snapshot.ts", + "sha256": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + }, + { + "path": "src/shared/process-table-snapshot-reader.ts", + "sha256": "7d35601a2a7bf7a9c5b4a897995e0e8a15775b4d35939f3dd1abe1a042bb1c5e" + }, + { + "path": "src/shared/process-table-index.ts", + "sha256": "30f236b7d0275f05673c0a7982bb0aa67c9319762749f5a583c5c31e51e31f36" + }, + { + "path": "src/shared/shell-process-detection.ts", + "sha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f" + }, + { + "path": "src/main/windows/windows-command-line-recovery-health.ts", + "sha256": "a135da3611d54fecb0dba52b4d81e06ca1532680424b55e7efa315b7c886ec33" + }, + { + "path": "src/shared/child-process/windows-command-line.ts", + "sha256": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + }, + { + "path": "src/shared/child-process/windows-cmd-shim-resolution.ts", + "sha256": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + }, + { + "path": "src/shared/child-process/spawn-resolution.ts", + "sha256": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + }, + { + "path": "src/shared/child-process/process-tree-kill-gate.ts", + "sha256": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + }, + { + "path": "src/shared/child-process/process-tree-termination.ts", + "sha256": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + }, + { + "path": "src/shared/child-process/bounded-output-sink.ts", + "sha256": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + }, + { + "path": "src/shared/child-process/child-termination-reporter.ts", + "sha256": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + }, + { + "path": "src/shared/child-process/process-spec.ts", + "sha256": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + }, + { + "path": "src/shared/child-process/run-process.ts", + "sha256": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + }, + { + "path": "src/shared/child-process/windows-system-binary.ts", + "sha256": "0c42c7c454534a0937f4153e9708aeb243bd930d130ffc6493ac42d21405606e" + }, + { + "path": "src/main/windows/windows-process-table-cim-scan.ts", + "sha256": "ecfa487a3139cc087eae78ff61bf358f7eb62aec3b20e9d064d96b6e37862ab0" + }, + { + "path": "src/main/windows/windows-process-table.ts", + "sha256": "de0565f724f49ca393bb55c1618e697ea34e98bfee132c196d0c3e97713aabe1" + }, + { + "path": "src/main/providers/windows-foreground-process-rows.ts", + "sha256": "374accddf905f5b60fb1be4010e8160bfaf7f1a3834ecaffbf0119e555c7a765" + }, + { + "path": "src/main/providers/windows-agent-foreground-process.ts", + "sha256": "a7056960617a9a5c740209a3a96280c4e57da69b7604e17f148c0c2195d0e5e9" + }, + { + "path": "src/shared/foreground-process-selection.ts", + "sha256": "18005d15f552cb8805148cebb330636f6cec0f8d98d2eca83444aef7794d3767" + }, + { + "path": "src/main/providers/agent-foreground-process-remote-evidence.ts", + "sha256": "c46feb5f19d873bbf08ff607a6a86a53a9d982ece7aa43e471aaad640621ad4d" + }, + { + "path": "src/main/providers/agent-foreground-process-batch.ts", + "sha256": "6f8c785ddd701883f7b44231b152a0c32b8a5a3db24529347085c1ac43127ce6" + }, + { + "path": "src/main/providers/agent-foreground-process.ts", + "sha256": "1f76c512e3c308959f11665c749e9d001da03661ad5bc2230a84125bc4aa8368" + }, + { + "path": "src/main/providers/windows-pty-job-membership.ts", + "sha256": "4b5031a7cbac09c967c4269a247311e09d6b2617a630dd1743bf0a9ddac37c5f" + }, + { + "path": "src/main/daemon/pty-subprocess/pty-shell-foreground-confirmation.ts", + "sha256": "aea01ded6b790b26a06314499eaa1db993c86660a076a710a73aa06a6ba1faee" + }, + { + "path": "src/main/providers/windows-cached-agent-revalidation.ts", + "sha256": "b7e60bf9e939b069488640b291bd025386cfa413b6431edd61563dba30bcc6be" + }, + { + "path": "src/main/providers/windows-console-attached-processes.ts", + "sha256": "63de2e44605c28139204c196cc1add202c723f4896c6f36757e92ab85a7fe065" + }, + { + "path": "src/main/daemon/pty-subprocess/foreground-fallback-process.ts", + "sha256": "a2a68908fa5f59a940d8b280073f5737374cb5f6d9938c64be2a4d26433d7827" + }, + { + "path": "src/main/daemon/pty-session-id.ts", + "sha256": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878" + }, + { + "path": "src/main/daemon/pty-subprocess/foreground-process-tracker.ts", + "sha256": "c29a04d412294f2ef3b46ec2fe7fd8f680ba00aadf11db4f9faea01b1b8c8df3" + }, + { + "path": "src/shared/terminal-exit-cause.ts", + "sha256": "cdbe03be86d26c6489a257e886cb6ca6b2301dc1a82752d6a028d66de46903a2" + }, + { + "path": "src/main/daemon/pty-subprocess/pre-listener-events.ts", + "sha256": "32b290969b16c2676bbc9801cf563fc1988f16131678ce3cf0c627ffeaf401a2" + }, + { + "path": "src/main/daemon/pty-subprocess/subprocess-handle.ts", + "sha256": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a" + } + ] + } + } +} diff --git a/docs/audits/native-pty-spawn-env-retention/fix.patch b/docs/audits/native-pty-spawn-env-retention/fix.patch new file mode 100644 index 00000000000..f09887d66ce --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/fix.patch @@ -0,0 +1,17 @@ +diff --git a/src/main/daemon/pty-subprocess/subprocess-handle.ts b/src/main/daemon/pty-subprocess/subprocess-handle.ts +index 974602dfe8..5d7ef16342 100644 +--- a/src/main/daemon/pty-subprocess/subprocess-handle.ts ++++ b/src/main/daemon/pty-subprocess/subprocess-handle.ts +@@ -24,4 +24,5 @@ export function createDaemonPtySubprocessHandle(args: { + startupAgentRecognition: RecognizedAgentProcess | null + }): SubprocessHandle { ++ const reportsChildExitStatus = args.reportsChildExitStatus + const proc = args.process + // node-pty exposes destroy at runtime but omits it from IPty. +@@ -57,5 +58,5 @@ export function createDaemonPtySubprocessHandle(args: { + exitCode, + signal, +- hostReportsChildExitStatus: args.reportsChildExitStatus ++ hostReportsChildExitStatus: reportsChildExitStatus + }) + }) diff --git a/docs/audits/native-pty-spawn-env-retention/node-results.json b/docs/audits/native-pty-spawn-env-retention/node-results.json new file mode 100644 index 00000000000..5c0f43bdd35 --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/node-results.json @@ -0,0 +1,532 @@ +{ + "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" + }, + "sourceHashLineEndings": "canonical LF", + "crlfLoaderControl": { + "syntheticCrlfReads": 2, + "identicalSourcesAndHashes": true + }, + "scope": "Actual wrapper, foreground tracker and pre-listener queue, with the installed node-pty event emitter and an inert native process. No native PTY, process scan, OS signal or window. Small object counts, no payload amplification.", + "nodePty": { + "version": "1.1.0", + "eventEmitterSha256": "f1c14613aa90c10def4ca7238329270871997eb26f919f7074dc25533e3e75dd" + }, + "reports": { + "before": { + "whileLive": { + "args": 1, + "env": 1 + }, + "afterOwnerDrop": { + "args": 0, + "env": 0 + }, + "controls": [ + "PATH retained", + "raw foreground receiver", + "pre-listener data/exit", + "status unavailable", + "signal cause", + "dead signal guard", + "idempotent dispose" + ] + }, + "after": { + "whileLive": { + "args": 0, + "env": 0 + }, + "afterOwnerDrop": { + "args": 0, + "env": 0 + }, + "controls": [ + "PATH retained", + "raw foreground receiver", + "pre-listener data/exit", + "status unavailable", + "signal cause", + "dead signal guard", + "idempotent dispose" + ] + } + }, + "versions": { + "before": { + "sourceHashes": { + "src/main/daemon/pty-subprocess/subprocess-handle.ts": { + "before": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e", + "after": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a" + } + }, + "bundleSha256": "a546917f2c969df3c010031537338121ed0de0ce6e60f45b39ab52dc776d4a7c", + "dependencies": [ + { + "path": "src/shared/pty-slave-line-discipline-echo.ts", + "sha256": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4" + }, + { + "path": "src/main/pty/node-pty-pts-name.ts", + "sha256": "aa1a50bd935895cbd1acc03fffd16ca735ede9d563cece849e7fdede99bc95a6" + }, + { + "path": "src/main/daemon/daemon-pty-size.ts", + "sha256": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741" + }, + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + }, + { + "path": "src/shared/cross-platform-path.ts", + "sha256": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3" + }, + { + "path": "src/shared/workspace-scope.ts", + "sha256": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581" + }, + { + "path": "src/shared/pty-session-id-format.ts", + "sha256": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b" + }, + { + "path": "src/shared/worktree/id.ts", + "sha256": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30" + }, + { + "path": "src/main/providers/agent-foreground-context-paths.ts", + "sha256": "df3ad7d74ff4048999231492aab490336078b915e9033cab77ab9e51b14c062f" + }, + { + "path": "src/shared/orca-cli-command-name.ts", + "sha256": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0" + }, + { + "path": "src/shared/tui-agent-config.ts", + "sha256": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad" + }, + { + "path": "src/shared/agent-node-entrypoint-identities.ts", + "sha256": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4" + }, + { + "path": "src/shared/print-mode-headless-command.ts", + "sha256": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a" + }, + { + "path": "src/shared/ante-headless-command.ts", + "sha256": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620" + }, + { + "path": "src/shared/prime-agent-headless-command.ts", + "sha256": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3" + }, + { + "path": "src/shared/agent-headless-command.ts", + "sha256": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87" + }, + { + "path": "src/shared/command-token-scanner.ts", + "sha256": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0" + }, + { + "path": "src/shared/agent-process-recognition.ts", + "sha256": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4" + }, + { + "path": "src/shared/synthetic-agent-title.ts", + "sha256": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + { + "path": "src/shared/foreground-wrapper-agent.ts", + "sha256": "87a2caa7daa616c96cf308a5413abf3eac21fe01411ff6bcc1e166d1cd22aa77" + }, + { + "path": "src/shared/process-table-snapshot.ts", + "sha256": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + }, + { + "path": "src/shared/process-table-snapshot-reader.ts", + "sha256": "7d35601a2a7bf7a9c5b4a897995e0e8a15775b4d35939f3dd1abe1a042bb1c5e" + }, + { + "path": "src/shared/process-table-index.ts", + "sha256": "30f236b7d0275f05673c0a7982bb0aa67c9319762749f5a583c5c31e51e31f36" + }, + { + "path": "src/shared/shell-process-detection.ts", + "sha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f" + }, + { + "path": "src/main/windows/windows-command-line-recovery-health.ts", + "sha256": "a135da3611d54fecb0dba52b4d81e06ca1532680424b55e7efa315b7c886ec33" + }, + { + "path": "src/shared/child-process/windows-command-line.ts", + "sha256": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + }, + { + "path": "src/shared/child-process/windows-cmd-shim-resolution.ts", + "sha256": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + }, + { + "path": "src/shared/child-process/spawn-resolution.ts", + "sha256": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + }, + { + "path": "src/shared/child-process/process-tree-kill-gate.ts", + "sha256": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + }, + { + "path": "src/shared/child-process/process-tree-termination.ts", + "sha256": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + }, + { + "path": "src/shared/child-process/bounded-output-sink.ts", + "sha256": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + }, + { + "path": "src/shared/child-process/child-termination-reporter.ts", + "sha256": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + }, + { + "path": "src/shared/child-process/process-spec.ts", + "sha256": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + }, + { + "path": "src/shared/child-process/run-process.ts", + "sha256": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + }, + { + "path": "src/shared/child-process/windows-system-binary.ts", + "sha256": "0c42c7c454534a0937f4153e9708aeb243bd930d130ffc6493ac42d21405606e" + }, + { + "path": "src/main/windows/windows-process-table-cim-scan.ts", + "sha256": "ecfa487a3139cc087eae78ff61bf358f7eb62aec3b20e9d064d96b6e37862ab0" + }, + { + "path": "src/main/windows/windows-process-table.ts", + "sha256": "de0565f724f49ca393bb55c1618e697ea34e98bfee132c196d0c3e97713aabe1" + }, + { + "path": "src/main/providers/windows-foreground-process-rows.ts", + "sha256": "374accddf905f5b60fb1be4010e8160bfaf7f1a3834ecaffbf0119e555c7a765" + }, + { + "path": "src/main/providers/windows-agent-foreground-process.ts", + "sha256": "a7056960617a9a5c740209a3a96280c4e57da69b7604e17f148c0c2195d0e5e9" + }, + { + "path": "src/shared/foreground-process-selection.ts", + "sha256": "18005d15f552cb8805148cebb330636f6cec0f8d98d2eca83444aef7794d3767" + }, + { + "path": "src/main/providers/agent-foreground-process-remote-evidence.ts", + "sha256": "c46feb5f19d873bbf08ff607a6a86a53a9d982ece7aa43e471aaad640621ad4d" + }, + { + "path": "src/main/providers/agent-foreground-process-batch.ts", + "sha256": "6f8c785ddd701883f7b44231b152a0c32b8a5a3db24529347085c1ac43127ce6" + }, + { + "path": "src/main/providers/agent-foreground-process.ts", + "sha256": "1f76c512e3c308959f11665c749e9d001da03661ad5bc2230a84125bc4aa8368" + }, + { + "path": "src/main/providers/windows-pty-job-membership.ts", + "sha256": "4b5031a7cbac09c967c4269a247311e09d6b2617a630dd1743bf0a9ddac37c5f" + }, + { + "path": "src/main/daemon/pty-subprocess/pty-shell-foreground-confirmation.ts", + "sha256": "aea01ded6b790b26a06314499eaa1db993c86660a076a710a73aa06a6ba1faee" + }, + { + "path": "src/main/providers/windows-cached-agent-revalidation.ts", + "sha256": "b7e60bf9e939b069488640b291bd025386cfa413b6431edd61563dba30bcc6be" + }, + { + "path": "src/main/providers/windows-console-attached-processes.ts", + "sha256": "63de2e44605c28139204c196cc1add202c723f4896c6f36757e92ab85a7fe065" + }, + { + "path": "src/main/daemon/pty-subprocess/foreground-fallback-process.ts", + "sha256": "a2a68908fa5f59a940d8b280073f5737374cb5f6d9938c64be2a4d26433d7827" + }, + { + "path": "src/main/daemon/pty-session-id.ts", + "sha256": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878" + }, + { + "path": "src/main/daemon/pty-subprocess/foreground-process-tracker.ts", + "sha256": "c29a04d412294f2ef3b46ec2fe7fd8f680ba00aadf11db4f9faea01b1b8c8df3" + }, + { + "path": "src/shared/terminal-exit-cause.ts", + "sha256": "cdbe03be86d26c6489a257e886cb6ca6b2301dc1a82752d6a028d66de46903a2" + }, + { + "path": "src/main/daemon/pty-subprocess/pre-listener-events.ts", + "sha256": "32b290969b16c2676bbc9801cf563fc1988f16131678ce3cf0c627ffeaf401a2" + }, + { + "path": "src/main/daemon/pty-subprocess/subprocess-handle.ts", + "sha256": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e" + } + ] + }, + "after": { + "sourceHashes": { + "src/main/daemon/pty-subprocess/subprocess-handle.ts": { + "before": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e", + "after": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a" + } + }, + "bundleSha256": "54e4ef70dd3262f94b679289440b882dfedacfb5e0af325d51d1ec2a2b455929", + "dependencies": [ + { + "path": "src/shared/pty-slave-line-discipline-echo.ts", + "sha256": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4" + }, + { + "path": "src/main/pty/node-pty-pts-name.ts", + "sha256": "aa1a50bd935895cbd1acc03fffd16ca735ede9d563cece849e7fdede99bc95a6" + }, + { + "path": "src/main/daemon/daemon-pty-size.ts", + "sha256": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741" + }, + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + }, + { + "path": "src/shared/cross-platform-path.ts", + "sha256": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3" + }, + { + "path": "src/shared/workspace-scope.ts", + "sha256": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581" + }, + { + "path": "src/shared/pty-session-id-format.ts", + "sha256": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b" + }, + { + "path": "src/shared/worktree/id.ts", + "sha256": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30" + }, + { + "path": "src/main/providers/agent-foreground-context-paths.ts", + "sha256": "df3ad7d74ff4048999231492aab490336078b915e9033cab77ab9e51b14c062f" + }, + { + "path": "src/shared/orca-cli-command-name.ts", + "sha256": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0" + }, + { + "path": "src/shared/tui-agent-config.ts", + "sha256": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad" + }, + { + "path": "src/shared/agent-node-entrypoint-identities.ts", + "sha256": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4" + }, + { + "path": "src/shared/print-mode-headless-command.ts", + "sha256": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a" + }, + { + "path": "src/shared/ante-headless-command.ts", + "sha256": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620" + }, + { + "path": "src/shared/prime-agent-headless-command.ts", + "sha256": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3" + }, + { + "path": "src/shared/agent-headless-command.ts", + "sha256": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87" + }, + { + "path": "src/shared/command-token-scanner.ts", + "sha256": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0" + }, + { + "path": "src/shared/agent-process-recognition.ts", + "sha256": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4" + }, + { + "path": "src/shared/synthetic-agent-title.ts", + "sha256": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + { + "path": "src/shared/foreground-wrapper-agent.ts", + "sha256": "87a2caa7daa616c96cf308a5413abf3eac21fe01411ff6bcc1e166d1cd22aa77" + }, + { + "path": "src/shared/process-table-snapshot.ts", + "sha256": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + }, + { + "path": "src/shared/process-table-snapshot-reader.ts", + "sha256": "7d35601a2a7bf7a9c5b4a897995e0e8a15775b4d35939f3dd1abe1a042bb1c5e" + }, + { + "path": "src/shared/process-table-index.ts", + "sha256": "30f236b7d0275f05673c0a7982bb0aa67c9319762749f5a583c5c31e51e31f36" + }, + { + "path": "src/shared/shell-process-detection.ts", + "sha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f" + }, + { + "path": "src/main/windows/windows-command-line-recovery-health.ts", + "sha256": "a135da3611d54fecb0dba52b4d81e06ca1532680424b55e7efa315b7c886ec33" + }, + { + "path": "src/shared/child-process/windows-command-line.ts", + "sha256": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + }, + { + "path": "src/shared/child-process/windows-cmd-shim-resolution.ts", + "sha256": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + }, + { + "path": "src/shared/child-process/spawn-resolution.ts", + "sha256": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + }, + { + "path": "src/shared/child-process/process-tree-kill-gate.ts", + "sha256": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + }, + { + "path": "src/shared/child-process/process-tree-termination.ts", + "sha256": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + }, + { + "path": "src/shared/child-process/bounded-output-sink.ts", + "sha256": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + }, + { + "path": "src/shared/child-process/child-termination-reporter.ts", + "sha256": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + }, + { + "path": "src/shared/child-process/process-spec.ts", + "sha256": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + }, + { + "path": "src/shared/child-process/run-process.ts", + "sha256": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + }, + { + "path": "src/shared/child-process/windows-system-binary.ts", + "sha256": "0c42c7c454534a0937f4153e9708aeb243bd930d130ffc6493ac42d21405606e" + }, + { + "path": "src/main/windows/windows-process-table-cim-scan.ts", + "sha256": "ecfa487a3139cc087eae78ff61bf358f7eb62aec3b20e9d064d96b6e37862ab0" + }, + { + "path": "src/main/windows/windows-process-table.ts", + "sha256": "de0565f724f49ca393bb55c1618e697ea34e98bfee132c196d0c3e97713aabe1" + }, + { + "path": "src/main/providers/windows-foreground-process-rows.ts", + "sha256": "374accddf905f5b60fb1be4010e8160bfaf7f1a3834ecaffbf0119e555c7a765" + }, + { + "path": "src/main/providers/windows-agent-foreground-process.ts", + "sha256": "a7056960617a9a5c740209a3a96280c4e57da69b7604e17f148c0c2195d0e5e9" + }, + { + "path": "src/shared/foreground-process-selection.ts", + "sha256": "18005d15f552cb8805148cebb330636f6cec0f8d98d2eca83444aef7794d3767" + }, + { + "path": "src/main/providers/agent-foreground-process-remote-evidence.ts", + "sha256": "c46feb5f19d873bbf08ff607a6a86a53a9d982ece7aa43e471aaad640621ad4d" + }, + { + "path": "src/main/providers/agent-foreground-process-batch.ts", + "sha256": "6f8c785ddd701883f7b44231b152a0c32b8a5a3db24529347085c1ac43127ce6" + }, + { + "path": "src/main/providers/agent-foreground-process.ts", + "sha256": "1f76c512e3c308959f11665c749e9d001da03661ad5bc2230a84125bc4aa8368" + }, + { + "path": "src/main/providers/windows-pty-job-membership.ts", + "sha256": "4b5031a7cbac09c967c4269a247311e09d6b2617a630dd1743bf0a9ddac37c5f" + }, + { + "path": "src/main/daemon/pty-subprocess/pty-shell-foreground-confirmation.ts", + "sha256": "aea01ded6b790b26a06314499eaa1db993c86660a076a710a73aa06a6ba1faee" + }, + { + "path": "src/main/providers/windows-cached-agent-revalidation.ts", + "sha256": "b7e60bf9e939b069488640b291bd025386cfa413b6431edd61563dba30bcc6be" + }, + { + "path": "src/main/providers/windows-console-attached-processes.ts", + "sha256": "63de2e44605c28139204c196cc1add202c723f4896c6f36757e92ab85a7fe065" + }, + { + "path": "src/main/daemon/pty-subprocess/foreground-fallback-process.ts", + "sha256": "a2a68908fa5f59a940d8b280073f5737374cb5f6d9938c64be2a4d26433d7827" + }, + { + "path": "src/main/daemon/pty-session-id.ts", + "sha256": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878" + }, + { + "path": "src/main/daemon/pty-subprocess/foreground-process-tracker.ts", + "sha256": "c29a04d412294f2ef3b46ec2fe7fd8f680ba00aadf11db4f9faea01b1b8c8df3" + }, + { + "path": "src/shared/terminal-exit-cause.ts", + "sha256": "cdbe03be86d26c6489a257e886cb6ca6b2301dc1a82752d6a028d66de46903a2" + }, + { + "path": "src/main/daemon/pty-subprocess/pre-listener-events.ts", + "sha256": "32b290969b16c2676bbc9801cf563fc1988f16131678ce3cf0c627ffeaf401a2" + }, + { + "path": "src/main/daemon/pty-subprocess/subprocess-handle.ts", + "sha256": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a" + } + ] + } + } +} diff --git a/docs/audits/native-pty-spawn-env-retention/reproduce.cjs b/docs/audits/native-pty-spawn-env-retention/reproduce.cjs new file mode 100644 index 00000000000..693f4ceb234 --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/reproduce.cjs @@ -0,0 +1,71 @@ +const assert = require('node:assert/strict') +const { createHash } = require('node:crypto') +const { readFileSync, writeFileSync } = require('node:fs') +const path = require('node:path') +const { canonicalLf, load, loadSources } = require('./sources.cjs') +const run = require('./scenario.cjs') + +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function') + +function checkCrlfLoader() { + const baseline = loadSources() + let syntheticCrlfReads = 0 + const crlf = loadSources({ + readText(file) { + syntheticCrlfReads++ + return canonicalLf(readFileSync(file, 'utf8')).replace(/\n/g, '\r\n') + } + }) + assert.equal(syntheticCrlfReads, 2) + assert.deepEqual(crlf.before, baseline.before) + assert.deepEqual(crlf.after, baseline.after) + assert.deepEqual(crlf.hashes, baseline.hashes) + return { syntheticCrlfReads, identicalSourcesAndHashes: true } +} + +async function main() { + const timer = setTimeout(() => { + process.stderr.write('proof deadline\n') + process.exit(2) + }, 15_000) + const crlfLoaderControl = checkCrlfLoader() + const reports = {} + const versions = {} + for (const [mode, fixed] of [ + ['before', false], + ['after', true] + ]) { + const loaded = await load(fixed) + reports[mode] = await run(loaded.create, fixed) + versions[mode] = { + sourceHashes: loaded.hashes, + bundleSha256: loaded.bundleSha256, + dependencies: loaded.dependencies + } + } + clearTimeout(timer) + const emitter = require.resolve('node-pty/lib/eventEmitter2') + const report = { + runtime: process.versions, + sourceHashLineEndings: 'canonical LF', + crlfLoaderControl, + scope: + 'Actual wrapper, foreground tracker and pre-listener queue, with the installed node-pty event emitter and an inert native process. No native PTY, process scan, OS signal or window. Small object counts, no payload amplification.', + nodePty: { + version: require('node-pty/package.json').version, + eventEmitterSha256: createHash('sha256') + .update(canonicalLf(readFileSync(emitter, 'utf8'))) + .digest('hex') + }, + reports, + versions + } + const name = process.versions.electron ? 'electron-results.json' : 'node-results.json' + writeFileSync(path.join(__dirname, name), `${JSON.stringify(report, null, 2)}\n`) + process.stdout.write(`${JSON.stringify({ runtime: process.versions.node, reports }, null, 2)}\n`) +} +main().catch((error) => { + process.stderr.write(`${error.stack}\n`) + process.exit(1) +}) diff --git a/docs/audits/native-pty-spawn-env-retention/scenario.cjs b/docs/audits/native-pty-spawn-env-retention/scenario.cjs new file mode 100644 index 00000000000..7726626cf93 --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/scenario.cjs @@ -0,0 +1,148 @@ +const assert = require('node:assert/strict') +const { EventEmitter2 } = require('node-pty/lib/eventEmitter2') + +function nativePort() { + const data = new EventEmitter2() + const exit = new EventEmitter2() + const calls = { writes: 0, resizes: 0, pauses: 0, resumes: 0, kills: 0, destroys: 0 } + return { + process: { + pid: 0, + process: 'audit-shell', + onData: data.event, + onExit: exit.event, + write() { + calls.writes++ + }, + resize() { + calls.resizes++ + }, + pause() { + calls.pauses++ + }, + resume() { + calls.resumes++ + }, + clear() {}, + kill() { + calls.kills++ + }, + destroy() { + calls.destroys++ + } + }, + emitData: (value) => data.fire(value), + emitExit: (value) => exit.fire(value), + calls + } +} + +function start(create, reportsChildExitStatus) { + const native = nativePort() + const env = { PATH: '/synthetic/audit/bin', RETENTION_FIXTURE: 'small ordinary field' } + const args = { + process: native.process, + shellPath: '/synthetic/audit-shell', + spawnCwd: '/synthetic', + requestedCwd: '/synthetic', + sessionId: 'native-env-audit', + startupAgentRecognition: null, + env, + startupCommandDeliveredInShellArgs: true, + reportsChildExitStatus + } + return { + handle: create(args), + native, + refs: { args: new WeakRef(args), env: new WeakRef(env) } + } +} + +async function collect() { + for (let round = 0; round < 6; round++) { + await new Promise(setImmediate) + global.gc() + } +} +function counts(refs) { + return Object.fromEntries( + ['args', 'env'].map((key) => [key, refs.filter((ref) => ref[key].deref()).length]) + ) +} + +async function run(create, fixed) { + const originalKill = process.kill + const nativeSignals = [] + process.kill = (...args) => { + nativeSignals.push(args) + throw new Error('No OS signal permitted in proof') + } + try { + let owner = start(create, true) + const refs = [owner.refs] + await collect() + const whileLive = counts(refs) + assert.deepEqual(whileLive, { args: fixed ? 0 : 1, env: fixed ? 0 : 1 }) + assert.equal(owner.handle.shellPathEnv, '/synthetic/audit/bin') + assert.equal(owner.handle.startupCommandDeliveredInShellArgs, true) + assert.equal(owner.handle.getForegroundProcess({ rawFallback: true }), 'audit-shell') + const output = [] + const exits = [] + owner.native.emitData('early-output') + owner.handle.onData((data) => output.push(data)) + owner.handle.onExit((code, cause) => exits.push({ code, cause })) + owner.handle.write('a') + owner.handle.resize(80, 24) + owner.handle.pause() + owner.handle.resume() + owner.native.emitExit({ exitCode: 7, signal: 0 }) + assert.deepEqual(exits, [{ code: 7, cause: { kind: 'exited', exitCode: 7 } }]) + assert.deepEqual(output, ['early-output']) + owner.handle.write('after-exit') + owner.handle.kill() + owner.handle.forceKill() + owner.handle.signal('SIGKILL') + assert.equal(owner.native.calls.writes, 1) + assert.equal(owner.native.calls.kills, 0) + owner.handle.dispose() + owner.handle.dispose() + assert.equal(owner.native.calls.destroys, 1) + owner = null + await collect() + const afterOwnerDrop = counts(refs) + assert.deepEqual(afterOwnerDrop, { args: 0, env: 0 }) + + const unavailable = start(create, false) + const unavailableExits = [] + unavailable.native.emitExit({ exitCode: 0, signal: 9 }) + unavailable.handle.onExit((code, cause) => unavailableExits.push({ code, cause })) + assert.deepEqual(unavailableExits, [ + { code: 0, cause: { kind: 'unknown', reason: 'host_status_unavailable' } } + ]) + unavailable.handle.dispose() + const signaled = start(create, true) + const signaledExits = [] + signaled.handle.onExit((code, cause) => signaledExits.push({ code, cause })) + signaled.native.emitExit({ exitCode: 0, signal: 9 }) + assert.deepEqual(signaledExits, [{ code: 0, cause: { kind: 'signaled', signal: 9 } }]) + signaled.handle.dispose() + assert.deepEqual(nativeSignals, []) + return { + whileLive, + afterOwnerDrop, + controls: [ + 'PATH retained', + 'raw foreground receiver', + 'pre-listener data/exit', + 'status unavailable', + 'signal cause', + 'dead signal guard', + 'idempotent dispose' + ] + } + } finally { + process.kill = originalKill + } +} + +module.exports = run diff --git a/docs/audits/native-pty-spawn-env-retention/source-versions.json b/docs/audits/native-pty-spawn-env-retention/source-versions.json new file mode 100644 index 00000000000..61a21fc8792 --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/source-versions.json @@ -0,0 +1,59 @@ +{ + "baselineHashes": { + "src/main/daemon/pty-subprocess/subprocess-handle.ts": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e" + }, + "fixedHashes": { + "src/main/daemon/pty-subprocess/subprocess-handle.ts": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a" + }, + "namedRefs": [ + { + "ref": "HEAD", + "revision": "b5fc27171c0273e0b9e2af873259e85724aea8c3", + "beforeSha256": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e", + "projectedSha256": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "matchesAuditBaseline": true, + "sameExitCallbackCapture": true, + "patchApplies": true + }, + { + "ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "revision": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "beforeSha256": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e", + "projectedSha256": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "matchesAuditBaseline": true, + "sameExitCallbackCapture": true, + "patchApplies": true + }, + { + "ref": "v1.4.198", + "revision": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "beforeSha256": "4623b60a0e362bb3cf218787573966aa056ee5fd1bdefc39fb5293446e4af70b", + "projectedSha256": "ed5665c33d0d4157836a5b97bd934c7c2894d84b9d8a905544fb19a7863dc262", + "matchesAuditBaseline": false, + "sameExitCallbackCapture": true, + "patchApplies": true + } + ], + "historicalRuntimeReproduced": false, + "callerProvenance": [ + { + "path": "src/main/daemon/pty-subprocess.ts", + "currentSha256": "9f32c99c79fd3a23fd61d572146f81c3680fb5cbc13337e4fb3179856a0d7dc1", + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": "9f32c99c79fd3a23fd61d572146f81c3680fb5cbc13337e4fb3179856a0d7dc1", + "v1.4.198": "9f32c99c79fd3a23fd61d572146f81c3680fb5cbc13337e4fb3179856a0d7dc1" + }, + { + "path": "src/main/daemon/pty-subprocess/native-pty-spawn.ts", + "currentSha256": "3752b779e06d7c004bb6acfba3188f1e5dfb589bc6a9bbe024f8b5e80dad68a5", + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": "3752b779e06d7c004bb6acfba3188f1e5dfb589bc6a9bbe024f8b5e80dad68a5", + "v1.4.198": "3752b779e06d7c004bb6acfba3188f1e5dfb589bc6a9bbe024f8b5e80dad68a5" + }, + { + "path": "src/main/daemon/pty-subprocess/spawn-environment.ts", + "currentSha256": "ceb93773a9c57f4b0ae246399600f648c298187b158b3cc5f0183ffb85d2a565", + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": "ceb93773a9c57f4b0ae246399600f648c298187b158b3cc5f0183ffb85d2a565", + "v1.4.198": "ceb93773a9c57f4b0ae246399600f648c298187b158b3cc5f0183ffb85d2a565" + } + ], + "sourceHashLineEndings": "canonical LF" +} diff --git a/docs/audits/native-pty-spawn-env-retention/sources.cjs b/docs/audits/native-pty-spawn-env-retention/sources.cjs new file mode 100644 index 00000000000..a4b54e7b028 --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/sources.cjs @@ -0,0 +1,97 @@ +const assert = require('node:assert/strict') +const { createHash } = require('node:crypto') +const { readFileSync } = require('node:fs') +const path = require('node:path') +const { resolve } = path +const Module = require('node:module') +const esbuild = require('esbuild') +const sha = (value) => createHash('sha256').update(value).digest('hex') +const { applyPatch, parsePatch, reversePatch } = require('diff') + +const canonicalLf = (text) => text.replace(/\r\n/g, '\n') + +function loadSources({ readText = (file) => readFileSync(file, 'utf8') } = {}) { + const root = resolve(__dirname, '../../..') + const expected = JSON.parse(readFileSync(resolve(__dirname, 'source-versions.json'), 'utf8')) + const parsed = parsePatch(canonicalLf(readText(resolve(__dirname, 'fix.patch')))) + 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 = canonicalLf(readText(absolute)) + 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}`) + assert.equal(hash(current), expected.fixedHashes[path], `Fixed source drift: ${path}`) + before.set(absolute, baseline) + after.set(absolute, current) + hashes[path] = { before: hash(baseline), after: hash(current) } + } + return { root, before, after, hashes } +} + +async function load(fixed) { + const { root, before, after, hashes } = loadSources() + const sourcePath = 'src/main/daemon/pty-subprocess/subprocess-handle.ts' + const selected = (fixed ? after : before).get(path.join(root, sourcePath)) + const build = await esbuild.build({ + entryPoints: [path.join(root, sourcePath)], + bundle: true, + platform: 'node', + format: 'cjs', + packages: 'external', + write: false, + metafile: true, + plugins: [ + { + name: 'capture-only-projection', + setup(build) { + build.onLoad({ filter: /subprocess-handle\.ts$/ }, (args) => { + assert.equal(args.path, path.join(root, sourcePath)) + return { contents: selected, loader: 'ts' } + }) + build.onResolve( + { filter: /(?:posix-pty-process-groups|posix-pty-foreground-group|windows-pty-job)$/ }, + (args) => ({ path: args.path, namespace: 'guard' }) + ) + build.onLoad({ filter: /.*/, namespace: 'guard' }, () => ({ + contents: ` + const unexpected = () => { throw new Error('No native termination permitted in proof') } + export const forceKillPosixPtyProcessGroups = unexpected + export const signalPosixPtyForegroundGroup = unexpected + export const terminatePtyJob = unexpected + export const isPtyJobOwnershipAvailable = unexpected + export const listPtyJobProcessIds = unexpected + `, + loader: 'js' + })) + } + } + ] + }) + const file = path.join(root, 'native-pty-env-proof.cjs') + const module_ = new Module(file, module) + module_.filename = file + module_.paths = Module._nodeModulePaths(root) + module_._compile(build.outputFiles[0].text, file) + return { + create: module_.exports.createDaemonPtySubprocessHandle, + hashes, + bundleSha256: sha(build.outputFiles[0].contents), + dependencies: Object.keys(build.metafile.inputs) + .filter((file) => file.startsWith('src/')) + .map((file) => ({ + path: file, + sha256: sha( + file === sourcePath ? selected : canonicalLf(readFileSync(path.join(root, file), 'utf8')) + ) + })) + } +} + +module.exports = { canonicalLf, load, loadSources } diff --git a/docs/audits/native-pty-spawn-env-retention/validation.json b/docs/audits/native-pty-spawn-env-retention/validation.json new file mode 100644 index 00000000000..46e4dbb7ee2 --- /dev/null +++ b/docs/audits/native-pty-spawn-env-retention/validation.json @@ -0,0 +1,53 @@ +{ + "tests": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/pty-subprocess-env-retention.test.ts src/main/daemon/pty-subprocess-handle-lifecycle.test.ts src/main/daemon/pty-subprocess-foreground-identity.test.ts src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts src/main/daemon/pty-subprocess-env-inheritance.test.ts src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts", + "passed": 114, + "skipped": 4, + "files": 6, + "exitCode": 0 + }, + "baselineOverlay": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/native-pty-spawn-env-retention/before.config.mjs src/main/daemon/pty-subprocess-env-retention.test.ts src/main/daemon/pty-subprocess-handle-lifecycle.test.ts", + "passed": 29, + "expectedFailed": 1, + "failure": "releases completed spawn arguments while a live handle still forwards data and exit", + "exitCode": 1 + }, + "typecheck": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm tc:node", + "exitCode": 0 + }, + "lint": { + "files": [ + "src/main/daemon/pty-subprocess/subprocess-handle.ts", + "src/main/daemon/pty-subprocess-env-retention.test.ts", + "docs/audits/native-pty-spawn-env-retention/sources.cjs", + "docs/audits/native-pty-spawn-env-retention/scenario.cjs", + "docs/audits/native-pty-spawn-env-retention/reproduce.cjs", + "docs/audits/native-pty-spawn-env-retention/before.config.mjs" + ], + "ordinary": "pnpm exec oxlint --no-ignore ", + "typeAware": "pnpm exec oxlint --no-ignore --type-aware --config config/oxlint-code-quality-type-aware.json --deny-warnings", + "exitCodes": [0, 0] + }, + "changedQuality": { + "command": "ORCA_BACKGROUND_LAUNCH=1 ORCA_CODE_QUALITY_BASE=b5fc27171c0273e0b9e2af873259e85724aea8c3 pnpm run check:code-quality:changed", + "exitCode": 0, + "changedFiles": 2, + "newFindings": 0 + }, + "proofs": { + "node": "ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=128 docs/audits/native-pty-spawn-env-retention/reproduce.cjs", + "electron": "Installed Electron executable with ELECTRON_RUN_AS_NODE=1, ORCA_BACKGROUND_LAUNCH=1, --expose-gc --max-old-space-size=128 and the same proof path.", + "exitCodes": [0, 0] + }, + "format": "Each source and artifact TS/CJS/MJS/MD/JSON file checked using oxfmt --stdin-filepath; patch excluded.", + "gitDiffCheckExitCode": 0, + "crlfLoaderControl": { + "runtimes": ["Node 26.6.0", "Electron 43.7.0 / Node 24.21.0"], + "syntheticCrlfReads": 2, + "identicalBeforeAfterSourcesAndHashes": true, + "productWrites": false, + "namedSourceHashesVerifiedAsCanonicalLf": true + } +} diff --git a/src/main/daemon/pty-subprocess-env-retention.test.ts b/src/main/daemon/pty-subprocess-env-retention.test.ts new file mode 100644 index 00000000000..c936fe28d30 --- /dev/null +++ b/src/main/daemon/pty-subprocess-env-retention.test.ts @@ -0,0 +1,128 @@ +import { EventEmitter } from 'node:events' +import { describe, expect, it, vi } from 'vitest' +import type * as pty from 'node-pty' +import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle' + +vi.mock('../pty/posix-pty-process-groups', () => ({ + forceKillPosixPtyProcessGroups: () => { + throw new Error('Unexpected native termination') + } +})) +vi.mock('../pty/posix-pty-foreground-group', () => ({ + signalPosixPtyForegroundGroup: () => { + throw new Error('Unexpected native signal') + } +})) + +function nativePort() { + const data = new EventEmitter() + const exit = new EventEmitter() + const process_ = { + pid: 0, + cols: 80, + rows: 24, + process: 'audit-shell', + handleFlowControl: false, + onData(listener: (value: string) => void) { + data.on('data', listener) + return { dispose: () => data.off('data', listener) } + }, + onExit(listener: (value: { exitCode: number; signal?: number }) => void) { + exit.on('exit', listener) + return { dispose: () => exit.off('exit', listener) } + }, + write: vi.fn(), + resize: vi.fn(), + pause: vi.fn(), + resume: vi.fn(), + clear: vi.fn(), + kill: vi.fn(), + destroy: vi.fn() + } satisfies pty.IPty & { destroy: () => void } + return { + process: process_, + emitData: (value: string) => data.emit('data', value), + emitExit: (exitCode: number, signal = 0) => exit.emit('exit', { exitCode, signal }) + } +} + +function start(reportsChildExitStatus = true) { + const native = nativePort() + const env = { PATH: 'audit-path', ORDINARY_FIELD: 'small fixture' } + const args = { + process: native.process, + shellPath: 'audit-shell', + spawnCwd: process.cwd(), + sessionId: 'native-env-retention', + startupAgentRecognition: null, + startupCommandDeliveredInShellArgs: true, + reportsChildExitStatus, + env + } + return { + handle: createDaemonPtySubprocessHandle(args), + native, + refs: { args: new WeakRef(args), env: new WeakRef(env) } + } +} + +async function collect(): Promise { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + for (let round = 0; round < 6; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } +} + +describe('native PTY spawn environment lifetime', () => { + it('releases completed spawn arguments while a live handle still forwards data and exit', async () => { + const fixture = start() + try { + await collect() + expect(fixture.refs.args.deref()).toBeUndefined() + expect(fixture.refs.env.deref()).toBeUndefined() + expect(fixture.handle.shellPathEnv).toBe('audit-path') + expect(fixture.handle.startupCommandDeliveredInShellArgs).toBe(true) + expect(fixture.handle.getForegroundProcess?.({ rawFallback: true })).toBe('audit-shell') + + fixture.native.emitData('early-output') + const onData = vi.fn() + const onExit = vi.fn() + fixture.handle.onData(onData) + fixture.handle.onExit(onExit) + expect(onData).toHaveBeenCalledWith('early-output') + fixture.handle.write('input') + expect(fixture.native.process.write).toHaveBeenCalledWith('input') + fixture.native.emitExit(7) + expect(onExit).toHaveBeenCalledWith(7, { kind: 'exited', exitCode: 7 }) + fixture.handle.write('after-exit') + fixture.handle.kill() + fixture.handle.forceKill() + expect(fixture.native.process.write).toHaveBeenCalledOnce() + } finally { + fixture.handle.dispose?.() + fixture.handle.dispose?.() + expect(fixture.native.process.destroy).toHaveBeenCalledOnce() + } + }) + + it.each([true, false])('preserves the spawn-time exit-status fact: %s', async (reportsStatus) => { + const fixture = start(reportsStatus) + try { + await collect() + fixture.native.emitExit(0, 9) + const onExit = vi.fn() + fixture.handle.onExit(onExit) + expect(onExit).toHaveBeenCalledWith( + 0, + reportsStatus + ? { kind: 'signaled', signal: 9 } + : { kind: 'unknown', reason: 'host_status_unavailable' } + ) + } finally { + fixture.handle.dispose?.() + } + }) +}) diff --git a/src/main/daemon/pty-subprocess/subprocess-handle.ts b/src/main/daemon/pty-subprocess/subprocess-handle.ts index 974602dfe84..5d7ef163424 100644 --- a/src/main/daemon/pty-subprocess/subprocess-handle.ts +++ b/src/main/daemon/pty-subprocess/subprocess-handle.ts @@ -23,6 +23,7 @@ export function createDaemonPtySubprocessHandle(args: { sessionId: string startupAgentRecognition: RecognizedAgentProcess | null }): SubprocessHandle { + const reportsChildExitStatus = args.reportsChildExitStatus const proc = args.process // node-pty exposes destroy at runtime but omits it from IPty. const nativeProc = proc as DisposableNativePty @@ -56,7 +57,7 @@ export function createDaemonPtySubprocessHandle(args: { events.acceptExit({ exitCode, signal, - hostReportsChildExitStatus: args.reportsChildExitStatus + hostReportsChildExitStatus: reportsChildExitStatus }) }) From 3c138bd8632f0d2fc72af0d0e50bd8cb4bd98a8b Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:19 -0700 Subject: [PATCH 16/59] Skip empty chunks in streamed agent text (#21142) * fix: skip empty chunks in streamed agent text * test: lint empty-delta retention reproducer --------- Co-authored-by: m4air --- .../empty-streamed-delta-retention/README.md | 36 + .../before.config.mjs | 30 + .../electron-results.json | 865 ++++++++++++++++++ .../empty-streamed-delta-retention/fix.patch | 7 + .../node-results.json | 864 +++++++++++++++++ .../reported.patch | 60 ++ .../reproduce.cjs | 66 ++ .../scenario.cjs | 156 ++++ .../source-versions.json | 77 ++ .../sources.cjs | 111 +++ .../validation.json | 50 + .../agent-session-delta-coalescer.ts | 4 +- ...gent-session-empty-delta-retention.test.ts | 126 +++ 13 files changed, 2451 insertions(+), 1 deletion(-) create mode 100644 docs/audits/empty-streamed-delta-retention/README.md create mode 100644 docs/audits/empty-streamed-delta-retention/before.config.mjs create mode 100644 docs/audits/empty-streamed-delta-retention/electron-results.json create mode 100644 docs/audits/empty-streamed-delta-retention/fix.patch create mode 100644 docs/audits/empty-streamed-delta-retention/node-results.json create mode 100644 docs/audits/empty-streamed-delta-retention/reported.patch create mode 100644 docs/audits/empty-streamed-delta-retention/reproduce.cjs create mode 100644 docs/audits/empty-streamed-delta-retention/scenario.cjs create mode 100644 docs/audits/empty-streamed-delta-retention/source-versions.json create mode 100644 docs/audits/empty-streamed-delta-retention/sources.cjs create mode 100644 docs/audits/empty-streamed-delta-retention/validation.json create mode 100644 src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts diff --git a/docs/audits/empty-streamed-delta-retention/README.md b/docs/audits/empty-streamed-delta-retention/README.md new file mode 100644 index 00000000000..d872c2ced4e --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/README.md @@ -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. diff --git a/docs/audits/empty-streamed-delta-retention/before.config.mjs b/docs/audits/empty-streamed-delta-retention/before.config.mjs new file mode 100644 index 00000000000..5ba89ae6d35 --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/before.config.mjs @@ -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 + } + } + ] +} diff --git a/docs/audits/empty-streamed-delta-retention/electron-results.json b/docs/audits/empty-streamed-delta-retention/electron-results.json new file mode 100644 index 00000000000..d4d8ef5a026 --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/electron-results.json @@ -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." +} diff --git a/docs/audits/empty-streamed-delta-retention/fix.patch b/docs/audits/empty-streamed-delta-retention/fix.patch new file mode 100644 index 00000000000..b450bfc909f --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/fix.patch @@ -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) ++ } diff --git a/docs/audits/empty-streamed-delta-retention/node-results.json b/docs/audits/empty-streamed-delta-retention/node-results.json new file mode 100644 index 00000000000..c614c1675e7 --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/node-results.json @@ -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." +} diff --git a/docs/audits/empty-streamed-delta-retention/reported.patch b/docs/audits/empty-streamed-delta-retention/reported.patch new file mode 100644 index 00000000000..51ded40b8a6 --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/reported.patch @@ -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() +- let nextOrder = 0 ++ const evictable = new Set() +@@ -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') diff --git a/docs/audits/empty-streamed-delta-retention/reproduce.cjs b/docs/audits/empty-streamed-delta-retention/reproduce.cjs new file mode 100644 index 00000000000..6a947ca003e --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/reproduce.cjs @@ -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 +}) diff --git a/docs/audits/empty-streamed-delta-retention/scenario.cjs b/docs/audits/empty-streamed-delta-retention/scenario.cjs new file mode 100644 index 00000000000..943d8130062 --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/scenario.cjs @@ -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 } diff --git a/docs/audits/empty-streamed-delta-retention/source-versions.json b/docs/audits/empty-streamed-delta-retention/source-versions.json new file mode 100644 index 00000000000..29f6bb0c1f2 --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/source-versions.json @@ -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." +} diff --git a/docs/audits/empty-streamed-delta-retention/sources.cjs b/docs/audits/empty-streamed-delta-retention/sources.cjs new file mode 100644 index 00000000000..36ec31bd52f --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/sources.cjs @@ -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 } diff --git a/docs/audits/empty-streamed-delta-retention/validation.json b/docs/audits/empty-streamed-delta-retention/validation.json new file mode 100644 index 00000000000..b53bf94f6ef --- /dev/null +++ b/docs/audits/empty-streamed-delta-retention/validation.json @@ -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." + } +} diff --git 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 index a3b32ca24f0..1d5608150ef 100644 --- 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 @@ -230,7 +230,9 @@ function appendWithinUtf8ByteLimit( if (deltaBytes <= available) { // The caller owns the per-stream array; append in place so each token is // amortized O(1) instead of copying the complete prefix on every delta. - current.push(delta) + if (delta.length > 0) { + current.push(delta) + } return { chunks: current, retainedBytes: currentBytes + deltaBytes, diff --git a/src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts b/src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts new file mode 100644 index 00000000000..4e2161b19a8 --- /dev/null +++ b/src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it, vi } from 'vitest' +import { createCodexStructuredItemStreams } from '../../codex/codex-structured-item-streams' +import { createAgentSessionDeltaCoalescer } from './agent-session-delta-coalescer' + +describe('empty streamed deltas', () => { + it('does not retain empty array slots through repeated Codex publications', () => { + const prefix = 'empty-delta-retention-prefix' + const streams = createCodexStructuredItemStreams({ + sink: { appendItem() {}, appendTombstone() {}, publish() {} }, + identityFor: () => ({ provider: 'codex', threadId: 'thread', turnId: 'turn', ordinal: 0 }), + schedule: () => () => {} + }) + const append = (delta: string) => + streams.handle('thread', 'item/agentMessage/delta', { itemId: 'item', delta }) + append(prefix) + try { + for (let batch = 0; batch < 4; batch += 1) { + for (let index = 0; index < 16_384; index += 1) { + append('') + } + expect(streams.flush()).toBe(true) + } + const originalJoin = Array.prototype.join + let retainedSlots = -1 + const spy = vi + .spyOn(Array.prototype, 'join') + .mockImplementation(function (this: unknown[], separator) { + // Byte counters cannot detect empty entries retained by the stream's chunk array. + if (separator === '' && this[0] === prefix) { + retainedSlots = this.length + } + return originalJoin.call(this, separator) + }) + let snapshot: ReturnType + try { + snapshot = streams.snapshot('thread', 'item') + } finally { + spy.mockRestore() + } + expect(retainedSlots).toBe(1) + expect(snapshot).toEqual({ + text: prefix, + observedBytes: Buffer.byteLength(prefix), + truncated: false + }) + append('é') + expect(streams.snapshot('thread', 'item')?.text).toBe(`${prefix}é`) + streams.forget('thread', 'item') + expect(streams.snapshot('thread', 'item')).toBeNull() + } finally { + streams.dispose() + } + }) + + it('preserves empty stream snapshots, scheduled publication and explicit flushes', () => { + const pending = new Set<() => void>() + const emitted: { key: string; text: string }[] = [] + const instance = createAgentSessionDeltaCoalescer({ + schedule: (run) => { + pending.add(run) + return () => { + pending.delete(run) + } + }, + emit: (key, text) => emitted.push({ key, text }) + }) + try { + expect(instance.append('empty', '')).toBe(true) + expect(instance.snapshot('empty')).toEqual({ + text: '', + observedBytes: 0, + truncated: false + }) + expect(pending.size).toBe(1) + expect(emitted).toEqual([]) + expect(instance.flushAll()).toBe(true) + expect(pending.size).toBe(0) + expect(emitted).toEqual([{ key: 'empty', text: '' }]) + instance.append('empty', 'visible') + instance.append('empty', '') + expect(pending.size).toBe(1) + expect(instance.flush('empty')).toBe(true) + expect(emitted.at(-1)).toEqual({ key: 'empty', text: 'visible' }) + instance.append('empty', '') + expect(instance.flushAll()).toBe(true) + expect(emitted).toHaveLength(3) + expect(emitted.at(-1)).toEqual({ key: 'empty', text: 'visible' }) + } finally { + instance.dispose() + } + expect(pending.size).toBe(0) + }) + + it('still refuses a new empty stream while the oldest output is backpressured', () => { + let accepting = false + const emitted: [string, string][] = [] + const instance = createAgentSessionDeltaCoalescer({ + maxStreams: 1, + schedule: () => () => {}, + emit: (key, text) => { + if (!accepting) { + return false + } + emitted.push([key, text]) + return true + } + }) + try { + instance.append('first', 'preserved') + expect(instance.append('second', '')).toBe(false) + expect(instance.snapshot('second')).toBeNull() + expect(instance.snapshot('first')?.text).toBe('preserved') + accepting = true + expect(instance.append('second', '')).toBe(true) + expect(instance.snapshot('first')).toBeNull() + expect(instance.snapshot('second')?.text).toBe('') + expect(instance.flushAll()).toBe(true) + expect(emitted).toEqual([ + ['first', 'preserved'], + ['second', ''] + ]) + } finally { + instance.dispose() + } + }) +}) From ab331253a0a8df91d66a4d2d68955ec74db90ec8 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:22 -0700 Subject: [PATCH 17/59] fix: release canceled working-directory waiter references (#21144) * fix: release canceled working-directory waiter references * test: normalize working-directory proof patch --------- Co-authored-by: m4air --- .../README.md | 59 + .../before.config.mjs | 24 + .../electron-results.json | 1260 +++++++++++++++++ .../fix.patch | 68 + .../node-results.json | 1259 ++++++++++++++++ .../reproduce.cjs | 107 ++ .../scenario.cjs | 235 +++ .../source-versions.json | 81 ++ .../sources.cjs | 100 ++ .../validation.json | 57 + ...ing-directory-validation-retention.test.ts | 225 +++ .../providers/working-directory-validation.ts | 62 +- 12 files changed, 3525 insertions(+), 12 deletions(-) create mode 100644 docs/audits/working-directory-wait-retention/README.md create mode 100644 docs/audits/working-directory-wait-retention/before.config.mjs create mode 100644 docs/audits/working-directory-wait-retention/electron-results.json create mode 100644 docs/audits/working-directory-wait-retention/fix.patch create mode 100644 docs/audits/working-directory-wait-retention/node-results.json create mode 100644 docs/audits/working-directory-wait-retention/reproduce.cjs create mode 100644 docs/audits/working-directory-wait-retention/scenario.cjs create mode 100644 docs/audits/working-directory-wait-retention/source-versions.json create mode 100644 docs/audits/working-directory-wait-retention/sources.cjs create mode 100644 docs/audits/working-directory-wait-retention/validation.json create mode 100644 src/main/providers/working-directory-validation-retention.test.ts diff --git a/docs/audits/working-directory-wait-retention/README.md b/docs/audits/working-directory-wait-retention/README.md new file mode 100644 index 00000000000..a7a2512e41a --- /dev/null +++ b/docs/audits/working-directory-wait-retention/README.md @@ -0,0 +1,59 @@ +# Canceled cwd validation waiter lifetime + +Canceled working-directory checks retained their AbortSignals while the shared native filesystem check remained pending. The change releases those caller references immediately while preserving the underlying native operation, raw-promise identity and callback ordering. + +**A small promise reaction and empty holder still remain per canceled wait until native settlement.** JavaScript promise reactions cannot be removed. This correction releases the signal, listener and caller resolvers; it does not establish a total bound on waiting metadata or explain an incident's memory magnitude. + +## Ownership and callers + +`src/main/providers/working-directory-validation.ts` keeps one pending validation per exact cwd. The raw `fs.stat` cannot be aborted, so the map entry and any UNC semaphore slot must survive caller cancellation until real settlement. Retiring them early would permit duplicate native work on the same stalled path. + +Previously each caller registered a `finally` callback that captured its signal. The fix keeps each caller's raw promise reaction in its original position, but that reaction now references a small holder. Abort or settlement removes the abort listener and clears the holder. A separate waiter factory prevents the first signal from sharing the map's cleanup closure. The redundant per-call rejection observer is removed; the existing map-level `then(forget, forget)` still handles native failure when every caller has left. + +The sole production importer is `pty-subprocess/spawn-preflight.ts:127–136`, through `local-pty-utils.ts`. `daemon-terminal-admission.ts` supplies a preparation signal, and `pty-subprocess.ts` forwards it to preflight. Ordinary daemon requests use a 30-second client timeout and a 5-second cancellation guard. A caller can therefore finish while the native filesystem operation remains pending across later requests. Those request timers do not bound the raw stat duration. + +No-signal callers still receive the exact original promise. Native map deletion, UNC lane ownership, WSL checks, creation reservations, shutdown and process authority are unchanged. The change stays on the execution host and applies to folder workspaces and git worktrees without a wire change. + +## Why each raw reaction remains + +Existing wait utilities were checked. A shared settlement observer changes this API's callback order: a raw-promise observer registered before a signal waiter can abort it before its raw result arrives. Moving every waiter behind an earlier shared observer would fulfill that waiter instead. The per-call holder preserves that order and synchronous cancellation. Six permanent regressions cover an external aborting observer before, between and after signal waiters, for native success and failure. + +## Before/after evidence + +The standalone proof bundles the actual validation module, UNC path parser and semaphore. Its native async stat is a deferred fixture; WSL subprocess operations throw if unexpectedly reached. It performs no actual cwd probe, remote filesystem access, native subprocess launch or app launch. The fixture contains 32 small canceled callers per outcome; no large payload is attached. + +| Before raw native settlement | Original Node 26.6.0 | Original Electron 43.7.0 / Node 24.21.0 | Fixed, both | +| ------------------------------- | -------------------- | --------------------------------------- | ----------- | +| Signals reachable | 32 | 32 | 0 | +| Cancellation errors reachable | 0 | 32 | 0 | +| Caller option objects reachable | 0 | 0 | 0 | +| Native stat calls | 1 | 1 | 1 | + +All measured caller objects collect after native settlement in both versions. Both native success and failure have the same lifetime result. Other controls pass on both runtimes: + +- Later live waiters receive the original operation's success or actionable error, and their listeners are removed. +- An already-aborted first caller still leaves the raw operation owned; no-signal callers share the same raw promise. +- Three canceled callers on one UNC host leave two native slots occupied. The third native operation starts only after one real completion. +- Forty-eight actual-module settlement/abort schedules and six raw-observer ordering cases match the original. +- Native rejection after all callers cancel produces no unhandled rejection. +- Synthetic CRLF source and patch reads produce identical reversed/fixed source and hashes without product writes. + +`sources.cjs` reverses `fix.patch` in memory and checks exact baseline and fixed SHA-256 values. Source hashes use canonical LF; reports include effective dependency and bundle hashes. No git history, ignored notes or copied production implementation is needed to rerun the proof. Each run has a 20-second deadline; the commands below set a 192 MiB heap limit. + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/working-directory-wait-retention/reproduce.cjs +``` + +For Electron, run its installed executable with the same flags and script path, setting `ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`. It runs in Node mode and creates no windows. + +## Source compatibility and validation + +The audited baseline module is byte-identical to main commit `291b4ddd6f1c1af480169885e0fda7f9c78ff053` and release `v1.4.198` (`e0826956fcfc532f5a1e55b5e081f2e57e553c43`). All nine recorded caller/dependency sources also match that main commit. This one-product-file change does not depend on the shared waiter helper or its auth-wait changes. `source-versions.json` records the exact comparisons; historical source equality is not a historical packaged-runtime reproduction. + +The fixed three-file regression run passed 31 tests. Reversing only this fix gives one expected first-caller retention failure and 30 passing controls, including the six raw-observer cases: + +```sh +ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/working-directory-wait-retention/before.config.mjs src/main/providers/working-directory-validation-retention.test.ts src/main/providers/working-directory-validation.test.ts src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts +``` + +`validation.json` records verification results. The measured retention requires a still-pending native operation; no affected-host capture, byte slope or attribution to #19831 is claimed. diff --git a/docs/audits/working-directory-wait-retention/before.config.mjs b/docs/audits/working-directory-wait-retention/before.config.mjs new file mode 100644 index 00000000000..888f37f0890 --- /dev/null +++ b/docs/audits/working-directory-wait-retention/before.config.mjs @@ -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/working-directory-wait-retention/sources.cjs') +) +const { before } = loadSources() +export default mergeConfig( + baseConfig, + defineConfig({ + plugins: [ + { + name: 'cwd-wait-before-fix', + enforce: 'pre', + transform(_code, id) { + const source = before.get(resolve(id.split('?')[0])) + return source === undefined ? undefined : { code: source, map: null } + } + } + ] + }) +) diff --git a/docs/audits/working-directory-wait-retention/electron-results.json b/docs/audits/working-directory-wait-retention/electron-results.json new file mode 100644 index 00000000000..24f1584f703 --- /dev/null +++ b/docs/audits/working-directory-wait-retention/electron-results.json @@ -0,0 +1,1260 @@ +{ + "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" + }, + "sourceHashLineEndings": "canonical LF", + "crlfLoaderControl": { + "syntheticCrlfReads": 2, + "identicalSourcesAndHashes": true + }, + "unhandledRejections": 0, + "reports": { + "before": { + "fulfilled": { + "beforeSettlement": { + "signal": 32, + "options": 0, + "error": 32 + }, + "afterSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "nativeCallsBeforeSettlement": 1, + "nativeCallsAfterFreshValidation": 2, + "lateResult": { + "status": "fulfilled" + } + }, + "rejected": { + "beforeSettlement": { + "signal": 32, + "options": 0, + "error": 32 + }, + "afterSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "nativeCallsBeforeSettlement": 1, + "nativeCallsAfterFreshValidation": 2, + "lateResult": { + "status": "rejected", + "message": "Working directory \"synthetic-validation-true\" does not exist. It may have been deleted or is on an unmounted volume (cwd: synthetic-validation-true, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + } + }, + "laneOwnership": { + "canceledWaits": 3, + "nativeCallsWhileBothSlotsOwned": 2, + "nativeCallsAfterOneRawCompletion": 3 + }, + "alreadyAborted": { + "nativeCalls": 1, + "noSignalPromiseIdentityPreserved": true + }, + "lateNativeRejection": { + "nativeRejectedAfterCallerCanceled": true + } + }, + "after": { + "fulfilled": { + "beforeSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "afterSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "nativeCallsBeforeSettlement": 1, + "nativeCallsAfterFreshValidation": 2, + "lateResult": { + "status": "fulfilled" + } + }, + "rejected": { + "beforeSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "afterSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "nativeCallsBeforeSettlement": 1, + "nativeCallsAfterFreshValidation": 2, + "lateResult": { + "status": "rejected", + "message": "Working directory \"synthetic-validation-true\" does not exist. It may have been deleted or is on an unmounted volume (cwd: synthetic-validation-true, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + } + }, + "laneOwnership": { + "canceledWaits": 3, + "nativeCallsWhileBothSlotsOwned": 2, + "nativeCallsAfterOneRawCompletion": 3 + }, + "alreadyAborted": { + "nativeCalls": 1, + "noSignalPromiseIdentityPreserved": true + }, + "lateNativeRejection": { + "nativeRejectedAfterCallerCanceled": true + } + } + }, + "matrix": [ + { + "reject": false, + "startedBefore": false, + "ticks": 0, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-0-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-0-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 0, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-0-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-0-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 1, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-1-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-1-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 1, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-1-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-1-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 2, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-2-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-2-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 2, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-2-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-2-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 3, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-3-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-3-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 3, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-3-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-3-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 4, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-4-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-4-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 4, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-4-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-4-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 8, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-8-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-8-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 8, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-8-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-8-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 0, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-0-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-0-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 0, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-0-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-0-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 1, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-1-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-1-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 1, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-1-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-1-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 2, + "abortFirst": false, + "before": { + "outcome": ["fulfilled"], + "calls": 1 + }, + "after": { + "outcome": ["fulfilled"], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 2, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-2-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-2-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 3, + "abortFirst": false, + "before": { + "outcome": ["fulfilled"], + "calls": 1 + }, + "after": { + "outcome": ["fulfilled"], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 3, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-3-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-3-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 4, + "abortFirst": false, + "before": { + "outcome": ["fulfilled"], + "calls": 1 + }, + "after": { + "outcome": ["fulfilled"], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 4, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-4-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-4-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 8, + "abortFirst": false, + "before": { + "outcome": ["fulfilled"], + "calls": 1 + }, + "after": { + "outcome": ["fulfilled"], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 8, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-8-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-8-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 0, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-0-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-0-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 0, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-0-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-0-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 1, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-1-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-1-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 1, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-1-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-1-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 2, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-2-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-2-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 2, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-2-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-2-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 3, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-3-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-3-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 3, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-3-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-3-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 4, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-4-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-4-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 4, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-4-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-4-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 8, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-8-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-8-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 8, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-8-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-8-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 0, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-0-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-0-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 0, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-0-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-0-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 1, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-1-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-1-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 1, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-1-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-1-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 2, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-2-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-2-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-2-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-2-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 2, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-2-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-2-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 3, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-3-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-3-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-3-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-3-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 3, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-3-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-3-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 4, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-4-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-4-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-4-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-4-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 4, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-4-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-4-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 8, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-8-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-8-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-8-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-8-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 8, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-8-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-8-true\" was canceled." + ], + "calls": 1 + } + } + ], + "observers": [ + { + "position": "before", + "reject": false, + "before": [["rejected", "WorkingDirectoryValidationAbortedError"]], + "after": [["rejected", "WorkingDirectoryValidationAbortedError"]] + }, + { + "position": "before", + "reject": true, + "before": [["rejected", "WorkingDirectoryValidationAbortedError"]], + "after": [["rejected", "WorkingDirectoryValidationAbortedError"]] + }, + { + "position": "between", + "reject": false, + "before": [["fulfilled"], ["rejected", "WorkingDirectoryValidationAbortedError"]], + "after": [["fulfilled"], ["rejected", "WorkingDirectoryValidationAbortedError"]] + }, + { + "position": "between", + "reject": true, + "before": [ + ["rejected", "Error"], + ["rejected", "WorkingDirectoryValidationAbortedError"] + ], + "after": [ + ["rejected", "Error"], + ["rejected", "WorkingDirectoryValidationAbortedError"] + ] + }, + { + "position": "after", + "reject": false, + "before": [["fulfilled"]], + "after": [["fulfilled"]] + }, + { + "position": "after", + "reject": true, + "before": [["rejected", "Error"]], + "after": [["rejected", "Error"]] + } + ], + "versions": { + "before": { + "sourceHashes": { + "src/main/providers/working-directory-validation.ts": { + "before": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "after": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212" + } + }, + "bundleSha256": "e4757e7593eccc8c48e69cc4a023983a020a25d0e31a90913d415a05030dff57", + "dependencies": [ + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + }, + { + "path": "src/shared/priority-semaphore.ts", + "sha256": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5" + }, + { + "path": "src/main/providers/working-directory-validation.ts", + "sha256": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043" + } + ] + }, + "after": { + "sourceHashes": { + "src/main/providers/working-directory-validation.ts": { + "before": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "after": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212" + } + }, + "bundleSha256": "d9ddd8e54dcde6b6a33d529b8ba4a6f94318980e469398b81a3a991923dd95b1", + "dependencies": [ + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + }, + { + "path": "src/shared/priority-semaphore.ts", + "sha256": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5" + }, + { + "path": "src/main/providers/working-directory-validation.ts", + "sha256": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212" + } + ] + } + }, + "scope": "Actual cwd validation and semaphore source. Native stat replaced by one bounded deferred fixture; no filesystem probe, WSL process or app. 32 canceled small signals per case. Native ownership retained until actual fixture settlement, including shared UNC lane. Small per-call reaction/empty-holder metadata still lives until native settlement. No synthetic large payload or incident RSS claim." +} diff --git a/docs/audits/working-directory-wait-retention/fix.patch b/docs/audits/working-directory-wait-retention/fix.patch new file mode 100644 index 00000000000..4b8dfc159f2 --- /dev/null +++ b/docs/audits/working-directory-wait-retention/fix.patch @@ -0,0 +1,68 @@ +diff --git a/src/main/providers/working-directory-validation.ts b/src/main/providers/working-directory-validation.ts +index 688cc85244..cf098cad2c 100644 +--- a/src/main/providers/working-directory-validation.ts ++++ b/src/main/providers/working-directory-validation.ts +@@ -165,12 +165 @@ export function validateWorkingDirectoryAsync( +- const shared = validation +- // The shared probe outlives this caller; keep it from surfacing as unhandled. +- void shared.catch(() => {}) +- return new Promise((resolve, reject) => { +- const onAbort = (): void => reject(new WorkingDirectoryValidationAbortedError(cwd)) +- if (signal.aborted) { +- onAbort() +- return +- } +- signal.addEventListener('abort', onAbort, { once: true }) +- shared.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort)) +- }) ++ return waitForWorkingDirectoryValidation(validation, cwd, signal) +@@ -204,0 +194,49 @@ async function probeWorkingDirectory(cwd: string): Promise { ++ ++type WorkingDirectoryWaiterHolder = { ++ waiter: { ++ signal: AbortSignal ++ onAbort: () => void ++ resolve: () => void ++ reject: (error: unknown) => void ++ } | null ++} ++ ++function takeWorkingDirectoryWaiter( ++ holder: WorkingDirectoryWaiterHolder ++): WorkingDirectoryWaiterHolder['waiter'] { ++ const waiter = holder.waiter ++ holder.waiter = null ++ waiter?.signal.removeEventListener('abort', waiter.onAbort) ++ return waiter ++} ++ ++// Keep reaction order while an abandoned caller's signal and resolver become collectible. ++function observeWorkingDirectoryValidation( ++ promise: Promise, ++ holder: WorkingDirectoryWaiterHolder ++): void { ++ void promise.then( ++ () => takeWorkingDirectoryWaiter(holder)?.resolve(), ++ (error: unknown) => takeWorkingDirectoryWaiter(holder)?.reject(error) ++ ) ++} ++ ++function waitForWorkingDirectoryValidation( ++ shared: Promise, ++ cwd: string, ++ signal: AbortSignal ++): Promise { ++ return new Promise((resolve, reject) => { ++ const holder: WorkingDirectoryWaiterHolder = { waiter: null } ++ const onAbort = (): void => { ++ takeWorkingDirectoryWaiter(holder)?.reject(new WorkingDirectoryValidationAbortedError(cwd)) ++ } ++ holder.waiter = { signal, onAbort, resolve, reject } ++ if (signal.aborted) { ++ onAbort() ++ return ++ } ++ signal.addEventListener('abort', onAbort, { once: true }) ++ observeWorkingDirectoryValidation(shared, holder) ++ }) ++} diff --git a/docs/audits/working-directory-wait-retention/node-results.json b/docs/audits/working-directory-wait-retention/node-results.json new file mode 100644 index 00000000000..d2dc9087473 --- /dev/null +++ b/docs/audits/working-directory-wait-retention/node-results.json @@ -0,0 +1,1259 @@ +{ + "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" + }, + "sourceHashLineEndings": "canonical LF", + "crlfLoaderControl": { + "syntheticCrlfReads": 2, + "identicalSourcesAndHashes": true + }, + "unhandledRejections": 0, + "reports": { + "before": { + "fulfilled": { + "beforeSettlement": { + "signal": 32, + "options": 0, + "error": 0 + }, + "afterSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "nativeCallsBeforeSettlement": 1, + "nativeCallsAfterFreshValidation": 2, + "lateResult": { + "status": "fulfilled" + } + }, + "rejected": { + "beforeSettlement": { + "signal": 32, + "options": 0, + "error": 0 + }, + "afterSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "nativeCallsBeforeSettlement": 1, + "nativeCallsAfterFreshValidation": 2, + "lateResult": { + "status": "rejected", + "message": "Working directory \"synthetic-validation-true\" does not exist. It may have been deleted or is on an unmounted volume (cwd: synthetic-validation-true, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + } + }, + "laneOwnership": { + "canceledWaits": 3, + "nativeCallsWhileBothSlotsOwned": 2, + "nativeCallsAfterOneRawCompletion": 3 + }, + "alreadyAborted": { + "nativeCalls": 1, + "noSignalPromiseIdentityPreserved": true + }, + "lateNativeRejection": { + "nativeRejectedAfterCallerCanceled": true + } + }, + "after": { + "fulfilled": { + "beforeSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "afterSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "nativeCallsBeforeSettlement": 1, + "nativeCallsAfterFreshValidation": 2, + "lateResult": { + "status": "fulfilled" + } + }, + "rejected": { + "beforeSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "afterSettlement": { + "signal": 0, + "options": 0, + "error": 0 + }, + "nativeCallsBeforeSettlement": 1, + "nativeCallsAfterFreshValidation": 2, + "lateResult": { + "status": "rejected", + "message": "Working directory \"synthetic-validation-true\" does not exist. It may have been deleted or is on an unmounted volume (cwd: synthetic-validation-true, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + } + }, + "laneOwnership": { + "canceledWaits": 3, + "nativeCallsWhileBothSlotsOwned": 2, + "nativeCallsAfterOneRawCompletion": 3 + }, + "alreadyAborted": { + "nativeCalls": 1, + "noSignalPromiseIdentityPreserved": true + }, + "lateNativeRejection": { + "nativeRejectedAfterCallerCanceled": true + } + } + }, + "matrix": [ + { + "reject": false, + "startedBefore": false, + "ticks": 0, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-0-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-0-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 0, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-0-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-0-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 1, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-1-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-1-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 1, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-1-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-1-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 2, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-2-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-2-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 2, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-2-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-2-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 3, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-3-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-3-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 3, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-3-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-3-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 4, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-4-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-4-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 4, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-4-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-4-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 8, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-8-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-8-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": false, + "startedBefore": false, + "ticks": 8, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-8-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-false-8-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 0, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-0-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-0-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 0, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-0-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-0-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 1, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-1-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-1-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 1, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-1-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-1-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 2, + "abortFirst": false, + "before": { + "outcome": ["fulfilled"], + "calls": 1 + }, + "after": { + "outcome": ["fulfilled"], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 2, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-2-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-2-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 3, + "abortFirst": false, + "before": { + "outcome": ["fulfilled"], + "calls": 1 + }, + "after": { + "outcome": ["fulfilled"], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 3, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-3-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-3-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 4, + "abortFirst": false, + "before": { + "outcome": ["fulfilled"], + "calls": 1 + }, + "after": { + "outcome": ["fulfilled"], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 4, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-4-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-4-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 8, + "abortFirst": false, + "before": { + "outcome": ["fulfilled"], + "calls": 1 + }, + "after": { + "outcome": ["fulfilled"], + "calls": 1 + } + }, + { + "reject": false, + "startedBefore": true, + "ticks": 8, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-8-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-false-true-8-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 0, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-0-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-0-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 0, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-0-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-0-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 1, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-1-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-1-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 1, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-1-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-1-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 2, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-2-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-2-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 2, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-2-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-2-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 3, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-3-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-3-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 3, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-3-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-3-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 4, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-4-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-4-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 4, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-4-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-4-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 8, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-8-false\" was canceled." + ], + "calls": 2 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-8-false\" was canceled." + ], + "calls": 2 + } + }, + { + "reject": true, + "startedBefore": false, + "ticks": 8, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-8-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-false-8-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 0, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-0-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-0-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 0, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-0-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-0-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 1, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-1-false\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-1-false\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 1, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-1-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-1-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 2, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-2-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-2-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-2-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-2-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 2, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-2-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-2-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 3, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-3-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-3-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-3-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-3-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 3, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-3-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-3-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 4, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-4-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-4-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-4-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-4-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 4, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-4-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-4-true\" was canceled." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 8, + "abortFirst": false, + "before": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-8-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-8-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "Error", + "Working directory \"matrix-true-true-8-false\" does not exist. It may have been deleted or is on an unmounted volume (cwd: matrix-true-true-8-false, arch: arm64, platform: darwin 25.6.0, orca: synthetic-cwd-validation-audit)." + ], + "calls": 1 + } + }, + { + "reject": true, + "startedBefore": true, + "ticks": 8, + "abortFirst": true, + "before": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-8-true\" was canceled." + ], + "calls": 1 + }, + "after": { + "outcome": [ + "rejected", + "WorkingDirectoryValidationAbortedError", + "Working directory validation for \"matrix-true-true-8-true\" was canceled." + ], + "calls": 1 + } + } + ], + "observers": [ + { + "position": "before", + "reject": false, + "before": [["rejected", "WorkingDirectoryValidationAbortedError"]], + "after": [["rejected", "WorkingDirectoryValidationAbortedError"]] + }, + { + "position": "before", + "reject": true, + "before": [["rejected", "WorkingDirectoryValidationAbortedError"]], + "after": [["rejected", "WorkingDirectoryValidationAbortedError"]] + }, + { + "position": "between", + "reject": false, + "before": [["fulfilled"], ["rejected", "WorkingDirectoryValidationAbortedError"]], + "after": [["fulfilled"], ["rejected", "WorkingDirectoryValidationAbortedError"]] + }, + { + "position": "between", + "reject": true, + "before": [ + ["rejected", "Error"], + ["rejected", "WorkingDirectoryValidationAbortedError"] + ], + "after": [ + ["rejected", "Error"], + ["rejected", "WorkingDirectoryValidationAbortedError"] + ] + }, + { + "position": "after", + "reject": false, + "before": [["fulfilled"]], + "after": [["fulfilled"]] + }, + { + "position": "after", + "reject": true, + "before": [["rejected", "Error"]], + "after": [["rejected", "Error"]] + } + ], + "versions": { + "before": { + "sourceHashes": { + "src/main/providers/working-directory-validation.ts": { + "before": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "after": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212" + } + }, + "bundleSha256": "e4757e7593eccc8c48e69cc4a023983a020a25d0e31a90913d415a05030dff57", + "dependencies": [ + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + }, + { + "path": "src/shared/priority-semaphore.ts", + "sha256": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5" + }, + { + "path": "src/main/providers/working-directory-validation.ts", + "sha256": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043" + } + ] + }, + "after": { + "sourceHashes": { + "src/main/providers/working-directory-validation.ts": { + "before": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "after": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212" + } + }, + "bundleSha256": "d9ddd8e54dcde6b6a33d529b8ba4a6f94318980e469398b81a3a991923dd95b1", + "dependencies": [ + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + }, + { + "path": "src/shared/priority-semaphore.ts", + "sha256": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5" + }, + { + "path": "src/main/providers/working-directory-validation.ts", + "sha256": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212" + } + ] + } + }, + "scope": "Actual cwd validation and semaphore source. Native stat replaced by one bounded deferred fixture; no filesystem probe, WSL process or app. 32 canceled small signals per case. Native ownership retained until actual fixture settlement, including shared UNC lane. Small per-call reaction/empty-holder metadata still lives until native settlement. No synthetic large payload or incident RSS claim." +} diff --git a/docs/audits/working-directory-wait-retention/reproduce.cjs b/docs/audits/working-directory-wait-retention/reproduce.cjs new file mode 100644 index 00000000000..66a413f5ddb --- /dev/null +++ b/docs/audits/working-directory-wait-retention/reproduce.cjs @@ -0,0 +1,107 @@ +const assert = require('node:assert/strict') +const { readFileSync, writeFileSync } = require('node:fs') +const path = require('node:path') +const { load, loadSources, canonicalLf } = require('./sources.cjs') +const { + fixtureKey, + lifetime, + laneOwnership, + ordering, + observerOrdering, + alreadyAborted, + canceledNativeRejection +} = require('./scenario.cjs') + +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function') +process.env.ORCA_APP_VERSION = 'synthetic-cwd-validation-audit' +function checkCrlfLoader() { + const baseline = loadSources() + let syntheticCrlfReads = 0 + const crlf = loadSources({ + readText(file) { + syntheticCrlfReads++ + return canonicalLf(readFileSync(file, 'utf8')).replace(/\n/g, '\r\n') + } + }) + assert.equal(syntheticCrlfReads, 2) + assert.deepEqual(crlf.before, baseline.before) + assert.deepEqual(crlf.after, baseline.after) + assert.deepEqual(crlf.hashes, baseline.hashes) + return { syntheticCrlfReads, identicalSourcesAndHashes: true } +} + +async function main() { + const timer = setTimeout(() => { + process.stderr.write('deadline\n') + process.exit(2) + }, 20_000) + const unhandled = [] + const recordUnhandled = (error) => unhandled.push(error) + process.on('unhandledRejection', recordUnhandled) + const crlfLoaderControl = checkCrlfLoader() + const loaded = { before: await load(false, fixtureKey), after: await load(true, fixtureKey) } + const reports = {} + for (const [mode, validation] of Object.entries(loaded)) { + reports[mode] = { + fulfilled: await lifetime(validation, mode === 'after', false), + rejected: await lifetime(validation, mode === 'after', true), + laneOwnership: await laneOwnership(validation), + alreadyAborted: await alreadyAborted(validation), + lateNativeRejection: await canceledNativeRejection(validation) + } + } + const matrix = [] + for (const reject of [false, true]) { + for (const startedBefore of [false, true]) { + for (const ticks of [0, 1, 2, 3, 4, 8]) { + for (const abortFirst of [false, true]) { + const args = [reject, startedBefore, ticks, abortFirst] + const before = await ordering(loaded.before, ...args) + const after = await ordering(loaded.after, ...args) + assert.deepEqual(after, before) + matrix.push({ reject, startedBefore, ticks, abortFirst, before, after }) + } + } + } + } + const observers = [] + for (const position of ['before', 'between', 'after']) { + for (const reject of [false, true]) { + const before = await observerOrdering(loaded.before, position, reject) + const after = await observerOrdering(loaded.after, position, reject) + assert.deepEqual(after, before) + observers.push({ position, reject, before, after }) + } + } + await new Promise(setImmediate) + assert.deepEqual(unhandled, []) + process.off('unhandledRejection', recordUnhandled) + clearTimeout(timer) + delete globalThis[fixtureKey] + const report = { + runtime: process.versions, + sourceHashLineEndings: 'canonical LF', + crlfLoaderControl, + unhandledRejections: unhandled.length, + reports, + matrix, + observers, + versions: Object.fromEntries( + Object.entries(loaded).map(([mode, value]) => [mode, value.versions]) + ), + scope: + 'Actual cwd validation and semaphore source. Native stat replaced by one bounded deferred fixture; no filesystem probe, WSL process or app. 32 canceled small signals per case. Native ownership retained until actual fixture settlement, including shared UNC lane. Small per-call reaction/empty-holder metadata still lives until native settlement. No synthetic large payload or incident RSS claim.' + } + writeFileSync( + path.join(__dirname, process.versions.electron ? 'electron-results.json' : 'node-results.json'), + `${JSON.stringify(report, null, 2)}\n` + ) + process.stdout.write( + `${JSON.stringify({ runtime: process.versions.node, reports, orderingCases: matrix.length, observerCases: observers.length, unhandledRejections: unhandled.length }, null, 2)}\n` + ) +} +main().catch((error) => { + process.stderr.write(`${error.stack}\n`) + process.exit(1) +}) diff --git a/docs/audits/working-directory-wait-retention/scenario.cjs b/docs/audits/working-directory-wait-retention/scenario.cjs new file mode 100644 index 00000000000..3e72892ee91 --- /dev/null +++ b/docs/audits/working-directory-wait-retention/scenario.cjs @@ -0,0 +1,235 @@ +const assert = require('node:assert/strict') +const { getEventListeners } = require('node:events') + +const fixtureKey = '__orcaWorkingDirectoryWaitFixture' +const validDirectory = { isDirectory: () => true } +async function collect() { + for (let round = 0; round < 6; round++) { + await new Promise(setImmediate) + global.gc() + } +} +const tick = async (count) => { + for (let i = 0; i < count; i++) { + await Promise.resolve() + } +} + +async function canceledWait(validation, cwd) { + const controller = new AbortController() + const options = { signal: controller.signal } + const refs = { signal: new WeakRef(controller.signal), options: new WeakRef(options) } + const waiting = validation.validateWorkingDirectoryAsync(cwd, options) + controller.abort() + await assert.rejects(waiting, (error) => { + refs.error = new WeakRef(error) + return error instanceof validation.WorkingDirectoryValidationAbortedError + }) + assert.equal(getEventListeners(controller.signal, 'abort').length, 0) + return refs +} +const counts = (refs) => + Object.fromEntries( + ['signal', 'options', 'error'].map((key) => [ + key, + refs.filter((ref) => ref[key].deref()).length + ]) + ) + +async function lifetime(validation, fixed, reject) { + const gate = Promise.withResolvers() + let statCalls = 0 + globalThis[fixtureKey] = { + stat() { + statCalls++ + return gate.promise + } + } + const cwd = `synthetic-validation-${reject}` + const refs = [] + for (let index = 0; index < 32; index++) { + refs.push(await canceledWait(validation, cwd)) + } + await collect() + const beforeSettlement = counts(refs) + assert.equal(statCalls, 1) + assert.equal(beforeSettlement.options, 0) + assert.equal(beforeSettlement.signal, fixed ? 0 : 32) + if (fixed) { + assert.equal(beforeSettlement.error, 0) + } + const controller = new AbortController() + const late = validation.validateWorkingDirectoryAsync(cwd, { signal: controller.signal }).then( + () => ({ status: 'fulfilled' }), + (error) => ({ status: 'rejected', message: error.message }) + ) + assert.equal(statCalls, 1) + assert.equal(getEventListeners(controller.signal, 'abort').length, 1) + if (reject) { + gate.reject(new Error('synthetic native failure')) + } else { + gate.resolve(validDirectory) + } + const lateResult = await late + assert.equal(lateResult.status, reject ? 'rejected' : 'fulfilled') + await collect() + const afterSettlement = counts(refs) + assert.deepEqual(afterSettlement, { signal: 0, options: 0, error: 0 }) + assert.equal(getEventListeners(controller.signal, 'abort').length, 0) + globalThis[fixtureKey] = { + stat() { + statCalls++ + return Promise.resolve(validDirectory) + } + } + await validation.validateWorkingDirectoryAsync(cwd) + assert.equal(statCalls, 2) + return { + beforeSettlement, + afterSettlement, + nativeCallsBeforeSettlement: 1, + nativeCallsAfterFreshValidation: statCalls, + lateResult + } +} + +async function laneOwnership(validation) { + const gates = [Promise.withResolvers(), Promise.withResolvers(), Promise.withResolvers()] + const started = [] + globalThis[fixtureKey] = { + stat(cwd) { + started.push(cwd) + return gates[started.length - 1].promise + } + } + const paths = Array.from({ length: 3 }, (_, index) => `\\\\synthetic-host\\dir-${index}`) + for (const cwd of paths) { + await canceledWait(validation, cwd) + } + await tick(8) + assert.equal(started.length, 2) + gates[0].resolve(validDirectory) + await new Promise(setImmediate) + assert.equal(started.length, 3) + gates[1].resolve(validDirectory) + gates[2].resolve(validDirectory) + await new Promise(setImmediate) + return { + canceledWaits: 3, + nativeCallsWhileBothSlotsOwned: 2, + nativeCallsAfterOneRawCompletion: 3 + } +} + +async function ordering(validation, reject, startedBefore, ticks, abortFirst) { + const gate = Promise.withResolvers() + let calls = 0 + globalThis[fixtureKey] = { + stat() { + calls++ + return calls === 1 ? gate.promise : Promise.resolve(validDirectory) + } + } + const cwd = `matrix-${reject}-${startedBefore}-${ticks}-${abortFirst}` + const anchor = validation.validateWorkingDirectoryAsync(cwd).catch(() => {}) + const controller = new AbortController() + const start = () => + validation.validateWorkingDirectoryAsync(cwd, { signal: controller.signal }).then( + () => ['fulfilled'], + (error) => ['rejected', error.name, error.message] + ) + let waiting = startedBefore ? start() : null + const settle = () => + reject ? gate.reject(new Error('raw failure')) : gate.resolve(validDirectory) + if (abortFirst) { + controller.abort() + } else { + settle() + } + await tick(ticks) + waiting ??= start() + if (abortFirst) { + settle() + } else { + controller.abort() + } + const outcome = await waiting + await anchor + await new Promise(setImmediate) + assert.equal(getEventListeners(controller.signal, 'abort').length, 0) + return { outcome, calls } +} + +async function observerOrdering(validation, position, reject) { + const gate = Promise.withResolvers() + globalThis[fixtureKey] = { stat: () => gate.promise } + const cwd = `observer-${position}-${reject}` + const raw = validation.validateWorkingDirectoryAsync(cwd) + const controller = new AbortController() + const start = () => + validation.validateWorkingDirectoryAsync(cwd, { signal: controller.signal }).then( + () => ['fulfilled'], + (error) => ['rejected', error.name] + ) + const waiting = [] + if (position !== 'before') { + waiting.push(start()) + } + const abortObserver = raw.then( + () => controller.abort(), + () => controller.abort() + ) + if (position !== 'after') { + waiting.push(start()) + } + if (reject) { + gate.reject(new Error('raw failure')) + } else { + gate.resolve(validDirectory) + } + const outcomes = await Promise.all(waiting) + await abortObserver + return outcomes +} + +async function alreadyAborted(validation) { + const gate = Promise.withResolvers() + let nativeCalls = 0 + globalThis[fixtureKey] = { + stat() { + nativeCalls++ + return gate.promise + } + } + const signal = AbortSignal.abort() + await assert.rejects( + validation.validateWorkingDirectoryAsync('pre-aborted', { signal }), + (error) => error instanceof validation.WorkingDirectoryValidationAbortedError + ) + assert.equal(getEventListeners(signal, 'abort').length, 0) + const raw = validation.validateWorkingDirectoryAsync('pre-aborted') + assert.equal(validation.validateWorkingDirectoryAsync('pre-aborted'), raw) + assert.equal(nativeCalls, 1) + gate.resolve(validDirectory) + await raw + return { nativeCalls, noSignalPromiseIdentityPreserved: true } +} + +async function canceledNativeRejection(validation) { + const gate = Promise.withResolvers() + globalThis[fixtureKey] = { stat: () => gate.promise } + await canceledWait(validation, 'late-native-rejection') + gate.reject(new Error('Native failure after all callers canceled')) + await new Promise(setImmediate) + return { nativeRejectedAfterCallerCanceled: true } +} + +module.exports = { + fixtureKey, + lifetime, + laneOwnership, + ordering, + observerOrdering, + alreadyAborted, + canceledNativeRejection +} diff --git a/docs/audits/working-directory-wait-retention/source-versions.json b/docs/audits/working-directory-wait-retention/source-versions.json new file mode 100644 index 00000000000..fbb478d2c02 --- /dev/null +++ b/docs/audits/working-directory-wait-retention/source-versions.json @@ -0,0 +1,81 @@ +{ + "baselineHashes": { + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043" + }, + "fixedHashes": { + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212" + }, + "namedRefs": [ + { + "ref": "HEAD", + "revision": "96970f9b6efe915f9578b765c93f3d06880ccc2d", + "sha256": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "matchesAuditBaseline": true, + "patchAppliesWithIdenticalFixedHash": true + }, + { + "ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "revision": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "matchesAuditBaseline": true, + "patchAppliesWithIdenticalFixedHash": true + }, + { + "ref": "v1.4.198", + "revision": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "matchesAuditBaseline": true, + "patchAppliesWithIdenticalFixedHash": true + } + ], + "sourceHashLineEndings": "canonical LF", + "historicalRuntimeReproduced": false, + "sharedWaiterDependency": false, + "callerProvenance": [ + { + "path": "src/main/daemon/pty-subprocess/spawn-preflight.ts", + "sha256": "54b19c44a2f7beabb7a1dcb817efc26be1c9b13462a410e82c2da88f5550ceb2", + "main291bSha256": "54b19c44a2f7beabb7a1dcb817efc26be1c9b13462a410e82c2da88f5550ceb2" + }, + { + "path": "src/main/providers/local-pty-utils.ts", + "sha256": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "main291bSha256": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25" + }, + { + "path": "src/main/daemon/pty-subprocess.ts", + "sha256": "9f32c99c79fd3a23fd61d572146f81c3680fb5cbc13337e4fb3179856a0d7dc1", + "main291bSha256": "9f32c99c79fd3a23fd61d572146f81c3680fb5cbc13337e4fb3179856a0d7dc1" + }, + { + "path": "src/main/daemon/daemon-terminal-admission.ts", + "sha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "main291bSha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251" + }, + { + "path": "src/main/daemon/daemon-pty-spawn-preparations.ts", + "sha256": "77c94466578a5066af069e45eb5e87a64b73b88580a13b2172b2ea846b434f4e", + "main291bSha256": "77c94466578a5066af069e45eb5e87a64b73b88580a13b2172b2ea846b434f4e" + }, + { + "path": "src/main/daemon/daemon-client-rpc-request.ts", + "sha256": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "main291bSha256": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b" + }, + { + "path": "src/main/daemon/client.ts", + "sha256": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "main291bSha256": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14" + }, + { + "path": "src/shared/priority-semaphore.ts", + "sha256": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "main291bSha256": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5" + }, + { + "path": "src/shared/wsl-paths.ts", + "sha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "main291bSha256": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a" + } + ] +} diff --git a/docs/audits/working-directory-wait-retention/sources.cjs b/docs/audits/working-directory-wait-retention/sources.cjs new file mode 100644 index 00000000000..c42f415cb05 --- /dev/null +++ b/docs/audits/working-directory-wait-retention/sources.cjs @@ -0,0 +1,100 @@ +const assert = require('node:assert/strict') +const { readFileSync } = require('node:fs') +const { createHash } = require('node:crypto') +const path = require('node:path') +const Module = require('node:module') +const esbuild = require('esbuild') +const root = path.resolve(__dirname, '../../..') +const read = (file) => readFileSync(file, 'utf8').replace(/\r\n/g, '\n') +const sha = (value) => createHash('sha256').update(value).digest('hex') +const sourcePath = 'src/main/providers/working-directory-validation.ts' +const { applyPatch, parsePatch, reversePatch } = require('diff') +const { resolve } = path +const canonicalLf = (text) => text.replace(/\r\n/g, '\n') + +function loadSources({ readText = (file) => readFileSync(file, 'utf8') } = {}) { + const root = resolve(__dirname, '../../..') + const expected = JSON.parse(readFileSync(resolve(__dirname, 'source-versions.json'), 'utf8')) + const parsed = parsePatch(canonicalLf(readText(resolve(__dirname, 'fix.patch')))) + 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 = canonicalLf(readText(absolute)) + 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}`) + assert.equal(hash(current), expected.fixedHashes[path], `Fixed source drift: ${path}`) + before.set(absolute, baseline) + after.set(absolute, current) + hashes[path] = { before: hash(baseline), after: hash(current) } + } + return { root, before, after, hashes } +} + +async function load(fixed, fixtureKey) { + const { before, after, hashes } = loadSources() + const original = before.get(path.join(root, sourcePath)) + const candidate = after.get(path.join(root, sourcePath)) + const build = await esbuild.build({ + entryPoints: [path.join(root, sourcePath)], + platform: 'node', + format: 'cjs', + bundle: true, + packages: 'external', + write: false, + metafile: true, + plugins: [ + { + name: 'validation-native-stat-port', + setup(builder) { + builder.onLoad({ filter: /working-directory-validation\.ts$/ }, (args) => { + assert.equal(args.path, path.join(root, sourcePath)) + return { contents: fixed ? candidate : original, loader: 'ts' } + }) + builder.onResolve({ filter: /^node:fs\/promises$/ }, () => ({ + path: 'native-stat', + namespace: 'fixture' + })) + builder.onResolve({ filter: /\/wsl$/ }, () => ({ + path: 'no-wsl-process', + namespace: 'fixture' + })) + builder.onLoad({ filter: /.*/, namespace: 'fixture' }, (args) => ({ + contents: + args.path === 'native-stat' + ? `export const stat = (...args) => globalThis[${JSON.stringify(fixtureKey)}].stat(...args)` + : "const unexpected = () => { throw new Error('No native WSL operation permitted') }; export const wslUncDirectoryExists = unexpected; export const wslUncDirectoryExistsAsync = unexpected", + loader: 'js' + })) + } + } + ] + }) + const filename = path.join(__dirname, 'in-memory-validation.cjs') + const loaded = new Module(filename, module) + loaded.filename = filename + loaded.paths = Module._nodeModulePaths(__dirname) + loaded._compile(build.outputFiles[0].text, filename) + return { + ...loaded.exports, + versions: { + sourceHashes: hashes, + bundleSha256: sha(build.outputFiles[0].contents), + dependencies: Object.keys(build.metafile.inputs) + .filter((file) => file.startsWith('src/')) + .map((file) => ({ + path: file, + sha256: sha( + file === sourcePath ? (fixed ? candidate : original) : read(path.join(root, file)) + ) + })) + } + } +} +module.exports = { load, loadSources, canonicalLf } diff --git a/docs/audits/working-directory-wait-retention/validation.json b/docs/audits/working-directory-wait-retention/validation.json new file mode 100644 index 00000000000..a08ebe46f00 --- /dev/null +++ b/docs/audits/working-directory-wait-retention/validation.json @@ -0,0 +1,57 @@ +{ + "tests": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/providers/working-directory-validation-retention.test.ts src/main/providers/working-directory-validation.test.ts src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts", + "passed": 31, + "files": 3, + "exitCode": 0, + "newRegressionCases": 12 + }, + "baselineOverlay": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/working-directory-wait-retention/before.config.mjs src/main/providers/working-directory-validation-retention.test.ts src/main/providers/working-directory-validation.test.ts src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts", + "passed": 30, + "expectedFailed": 1, + "failure": "releases the first caller and subsequent canceled callers while their native stat stays owned", + "exitCode": 1 + }, + "typecheck": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm tc:node", + "exitCode": 0 + }, + "lint": { + "files": [ + "src/main/providers/working-directory-validation.ts", + "src/main/providers/working-directory-validation-retention.test.ts", + "docs/audits/working-directory-wait-retention/sources.cjs", + "docs/audits/working-directory-wait-retention/scenario.cjs", + "docs/audits/working-directory-wait-retention/reproduce.cjs", + "docs/audits/working-directory-wait-retention/before.config.mjs" + ], + "ordinary": "pnpm exec oxlint --no-ignore ", + "typeAware": "pnpm exec oxlint --no-ignore --type-aware --config config/oxlint-code-quality-type-aware.json --deny-warnings", + "exitCodes": [0, 0] + }, + "changedQuality": { + "command": "ORCA_BACKGROUND_LAUNCH=1 ORCA_CODE_QUALITY_BASE=96970f9b6efe915f9578b765c93f3d06880ccc2d pnpm run check:code-quality:changed", + "exitCode": 0, + "changedFiles": 2, + "newFindings": 0 + }, + "proofs": { + "node": "ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/working-directory-wait-retention/reproduce.cjs", + "electron": "Installed Electron executable with ELECTRON_RUN_AS_NODE=1 and ORCA_BACKGROUND_LAUNCH=1; same flags and proof path.", + "exitCodes": [0, 0], + "nativeSettlementOrderingCasesPerRuntime": 48, + "externalRawObserverCasesPerRuntime": 6, + "unhandledRejectionsPerRuntime": 0, + "crlfLoaderControlPerRuntime": true + }, + "format": "All product TS and artifact CJS/MJS/MD/JSON checked with oxfmt --stdin-filepath; patch excluded.", + "gitDiffCheckExitCode": 0, + "historicalCompatibility": "All three named baseline refs accept fix.patch and produce identical fixed SHA-256; all nine recorded provenance sources match main291b. No shared-waiter dependency.", + "artifactNormalization": { + "change": "Regenerated fix.patch with zero context so stored context blank lines do not appear as trailing whitespace when checked as a new artifact. Product/tests unchanged.", + "proofs": "Both Node/Electron 54-case ordering/lifecycle runs pass with the regenerated patch; formatted result files are byte-identical to the prior capture.", + "quality": "All six published code files explicitly scanned through five quality configurations with --no-ignore. Ordinary/type-aware/React/design scans pass; whole-file casting scan reports one unchanged assertion at product line84 present in the baseline. Root changed-lines gate since96970f9b passes all five scans across11 changed files with zero new findings.", + "whitespace": "All 12 publication files checked as complete additions; no whitespace diagnostics." + } +} diff --git a/src/main/providers/working-directory-validation-retention.test.ts b/src/main/providers/working-directory-validation-retention.test.ts new file mode 100644 index 00000000000..ab711bdadf3 --- /dev/null +++ b/src/main/providers/working-directory-validation-retention.test.ts @@ -0,0 +1,225 @@ +import { getEventListeners } from 'node:events' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { stat } = vi.hoisted(() => ({ + stat: vi.fn<() => Promise<{ isDirectory: () => boolean }>>() +})) +vi.mock('node:fs/promises', () => ({ stat })) +vi.mock('../wsl', () => ({ + wslUncDirectoryExists: () => { + throw new Error('Unexpected WSL probe') + }, + wslUncDirectoryExistsAsync: () => { + throw new Error('Unexpected WSL probe') + } +})) + +import { + _resetWorkingDirectoryValidationStateForTest, + validateWorkingDirectoryAsync as validate, + WorkingDirectoryValidationAbortedError +} from './working-directory-validation' + +const directory = { isDirectory: () => true } +const cwd = 'synthetic-cwd-wait-retention' + +function pendingStat() { + const gate = Promise.withResolvers() + stat.mockReturnValue(gate.promise) + return gate +} + +async function canceledWait(path = cwd) { + const controller = new AbortController() + const signal = new WeakRef(controller.signal) + const waiting = validate(path, { signal: controller.signal }) + controller.abort() + try { + await waiting + throw new Error('Expected cancellation') + } catch (error) { + if (!(error instanceof WorkingDirectoryValidationAbortedError)) { + throw error + } + expect(getEventListeners(controller.signal, 'abort')).toHaveLength(0) + return { signal, error: new WeakRef(error) } + } +} + +async function collect(): Promise { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + for (let round = 0; round < 6; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } +} + +beforeEach(() => { + stat.mockReset() + _resetWorkingDirectoryValidationStateForTest() +}) +afterEach(() => vi.restoreAllMocks()) + +describe('working directory validation waiter lifetime', () => { + it('releases the first caller and subsequent canceled callers while their native stat stays owned', async () => { + const gate = pendingStat() + try { + const first = await canceledWait() + const later: Awaited>[] = [] + for (let index = 0; index < 31; index += 1) { + later.push(await canceledWait()) + } + await collect() + expect(first.signal.deref()).toBeUndefined() + expect(first.error.deref()).toBeUndefined() + expect(later.filter((ref) => ref.signal.deref() || ref.error.deref())).toHaveLength(0) + expect(stat).toHaveBeenCalledOnce() + + const staying = validate(cwd) + expect(stat).toHaveBeenCalledOnce() + gate.resolve(directory) + await staying + } finally { + gate.resolve(directory) + } + }) + + it.each([false, true])( + 'cleans a successful or rejected live waiter: reject=%s', + async (reject) => { + const gate = pendingStat() + const controller = new AbortController() + const raw = validate(cwd) + expect(validate(cwd)).toBe(raw) + const rawResult = raw.catch((error: unknown) => error) + const waiting = validate(cwd, { signal: controller.signal }).catch((error: unknown) => error) + expect(getEventListeners(controller.signal, 'abort')).toHaveLength(1) + if (reject) { + gate.reject(new Error('Native stat failed')) + } else { + gate.resolve(directory) + } + const [rawValue, callerValue] = await Promise.all([rawResult, waiting]) + expect(callerValue).toBe(rawValue) + expect(getEventListeners(controller.signal, 'abort')).toHaveLength(0) + if (reject) { + expect(callerValue).toBeInstanceOf(Error) + } else { + expect(callerValue).toBeUndefined() + } + stat.mockResolvedValue(directory) + await validate(cwd) + expect(stat).toHaveBeenCalledTimes(2) + } + ) + + it('preserves the raw operation when the first caller is already aborted', async () => { + const gate = pendingStat() + try { + const signal = AbortSignal.abort() + await expect(validate(cwd, { signal })).rejects.toBeInstanceOf( + WorkingDirectoryValidationAbortedError + ) + const first = validate(cwd) + expect(validate(cwd)).toBe(first) + expect(stat).toHaveBeenCalledOnce() + expect(getEventListeners(signal, 'abort')).toHaveLength(0) + gate.resolve(directory) + await first + } finally { + gate.resolve(directory) + } + }) + + it('handles native rejection after every caller has already canceled', async () => { + const unhandled: unknown[] = [] + const recordUnhandled = (error: unknown): void => { + unhandled.push(error) + } + process.on('unhandledRejection', recordUnhandled) + const gate = pendingStat() + try { + await canceledWait() + gate.reject(new Error('Late native failure')) + await new Promise((resolve) => setImmediate(resolve)) + expect(unhandled).toEqual([]) + stat.mockResolvedValue(directory) + await validate(cwd) + expect(stat).toHaveBeenCalledTimes(2) + } finally { + gate.resolve(directory) + process.off('unhandledRejection', recordUnhandled) + } + }) + + it('keeps UNC slots occupied after caller cancellation until native settlement', async () => { + const gates = Array.from({ length: 3 }, () => Promise.withResolvers()) + let calls = 0 + stat.mockImplementation(() => { + const gate = gates[calls++] + if (!gate) { + throw new Error('Unexpected native stat') + } + return gate.promise + }) + try { + for (let index = 0; index < 3; index += 1) { + await canceledWait(`\\\\synthetic-host\\path-${index}`) + } + expect(stat).toHaveBeenCalledTimes(2) + gates[0].resolve(directory) + await new Promise((resolve) => setImmediate(resolve)) + expect(stat).toHaveBeenCalledTimes(3) + } finally { + for (const gate of gates) { + gate.resolve(directory) + } + await new Promise((resolve) => setImmediate(resolve)) + } + }) + + it.each( + (['before', 'between', 'after'] as const).flatMap((position) => + [false, true].map((reject) => ({ position, reject })) + ) + )( + 'preserves an external raw observer at $position with reject=$reject', + async ({ position, reject }) => { + const gate = pendingStat() + const raw = validate(cwd) + const controller = new AbortController() + const start = () => + validate(cwd, { signal: controller.signal }).then( + () => 'fulfilled', + (error: unknown) => (error instanceof Error ? error.name : 'unknown') + ) + const waiters: Promise[] = [] + if (position !== 'before') { + waiters.push(start()) + } + const abortObserver = raw.then( + () => controller.abort(), + () => controller.abort() + ) + if (position !== 'after') { + waiters.push(start()) + } + if (reject) { + gate.reject(new Error('Native stat failed')) + } else { + gate.resolve(directory) + } + const rawOutcome = reject ? 'Error' : 'fulfilled' + expect(await Promise.all(waiters)).toEqual( + position === 'before' + ? ['WorkingDirectoryValidationAbortedError'] + : position === 'between' + ? [rawOutcome, 'WorkingDirectoryValidationAbortedError'] + : [rawOutcome] + ) + await abortObserver + } + ) +}) diff --git a/src/main/providers/working-directory-validation.ts b/src/main/providers/working-directory-validation.ts index 688cc852446..cf098cad2c3 100644 --- a/src/main/providers/working-directory-validation.ts +++ b/src/main/providers/working-directory-validation.ts @@ -162,18 +162,7 @@ export function validateWorkingDirectoryAsync( if (!signal) { return validation } - const shared = validation - // The shared probe outlives this caller; keep it from surfacing as unhandled. - void shared.catch(() => {}) - return new Promise((resolve, reject) => { - const onAbort = (): void => reject(new WorkingDirectoryValidationAbortedError(cwd)) - if (signal.aborted) { - onAbort() - return - } - signal.addEventListener('abort', onAbort, { once: true }) - shared.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort)) - }) + return waitForWorkingDirectoryValidation(validation, cwd, signal) } function validateWorkingDirectoryUncached(cwd: string): Promise { @@ -202,3 +191,52 @@ async function probeWorkingDirectory(cwd: string): Promise { throw new Error(`Working directory "${cwd}" is not a directory.`) } } + +type WorkingDirectoryWaiterHolder = { + waiter: { + signal: AbortSignal + onAbort: () => void + resolve: () => void + reject: (error: unknown) => void + } | null +} + +function takeWorkingDirectoryWaiter( + holder: WorkingDirectoryWaiterHolder +): WorkingDirectoryWaiterHolder['waiter'] { + const waiter = holder.waiter + holder.waiter = null + waiter?.signal.removeEventListener('abort', waiter.onAbort) + return waiter +} + +// Keep reaction order while an abandoned caller's signal and resolver become collectible. +function observeWorkingDirectoryValidation( + promise: Promise, + holder: WorkingDirectoryWaiterHolder +): void { + void promise.then( + () => takeWorkingDirectoryWaiter(holder)?.resolve(), + (error: unknown) => takeWorkingDirectoryWaiter(holder)?.reject(error) + ) +} + +function waitForWorkingDirectoryValidation( + shared: Promise, + cwd: string, + signal: AbortSignal +): Promise { + return new Promise((resolve, reject) => { + const holder: WorkingDirectoryWaiterHolder = { waiter: null } + const onAbort = (): void => { + takeWorkingDirectoryWaiter(holder)?.reject(new WorkingDirectoryValidationAbortedError(cwd)) + } + holder.waiter = { signal, onAbort, resolve, reject } + if (signal.aborted) { + onAbort() + return + } + signal.addEventListener('abort', onAbort, { once: true }) + observeWorkingDirectoryValidation(shared, holder) + }) +} From 14654d03cb14abbcb6d442b360c403c2cc778bcd Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:25 -0700 Subject: [PATCH 18/59] fix: release completed SSH writer queue entries (#21150) Co-authored-by: m4air --- .../ssh-writer-consumed-prefix/README.md | 54 +++ .../before.config.mjs | 24 ++ .../electron-results.json | 382 ++++++++++++++++++ .../ssh-writer-consumed-prefix/fix.patch | 18 + .../node-results.json | 381 +++++++++++++++++ .../ssh-writer-consumed-prefix/reproduce.cjs | 57 +++ .../ssh-writer-consumed-prefix/scenario.cjs | 260 ++++++++++++ .../source-versions.json | 208 ++++++++++ .../ssh-writer-consumed-prefix/sources.cjs | 102 +++++ .../validation.json | 157 +++++++ .../ssh-multiplexer-writer-lane-scheduler.ts | 12 +- .../ssh-multiplexer-writer-retention.test.ts | 159 ++++++++ 12 files changed, 1810 insertions(+), 4 deletions(-) create mode 100644 docs/audits/ssh-writer-consumed-prefix/README.md create mode 100644 docs/audits/ssh-writer-consumed-prefix/before.config.mjs create mode 100644 docs/audits/ssh-writer-consumed-prefix/electron-results.json create mode 100644 docs/audits/ssh-writer-consumed-prefix/fix.patch create mode 100644 docs/audits/ssh-writer-consumed-prefix/node-results.json create mode 100644 docs/audits/ssh-writer-consumed-prefix/reproduce.cjs create mode 100644 docs/audits/ssh-writer-consumed-prefix/scenario.cjs create mode 100644 docs/audits/ssh-writer-consumed-prefix/source-versions.json create mode 100644 docs/audits/ssh-writer-consumed-prefix/sources.cjs create mode 100644 docs/audits/ssh-writer-consumed-prefix/validation.json create mode 100644 src/main/ssh/ssh-multiplexer-writer-retention.test.ts diff --git a/docs/audits/ssh-writer-consumed-prefix/README.md b/docs/audits/ssh-writer-consumed-prefix/README.md new file mode 100644 index 00000000000..cffd00155f9 --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/README.md @@ -0,0 +1,54 @@ +# Completed SSH writes retained behind a rolling backlog + +The SSH multiplexer lane scheduler advanced its read index without clearing consumed entries. A lane that stayed nonempty retained every completed `WriterEntry`, its encoded buffer and its settlement callback. The writer had already released those entries from its byte and frame counters, so admission limits did not bound this consumed prefix. Fully draining the lane or disposing the multiplexer released it. + +The fix clears the selected slot and compacts a consumed prefix after at least 1,024 selections when it occupies at least half the array. This follows the existing `RelayFrameBuffer` pattern. Lane ordering, fairness, admission limits, transport settlement and in-flight ownership are unchanged. `clear()` returns only remaining live entries. + +## Actual caller and ownership + +The ordinary-lane proof executes `writeToSshPtyWithSettlement` → `SshChannelMultiplexer.notifyWithSettlement` → `SshMultiplexerTransportWriter` → `SshMultiplexerWriterLaneScheduler`. `SshPtyProvider` exposes the same helper through its RPC operations. The control-lane proof uses the `git.responseAck` notification shape emitted by `requestGitStreamable`. + +`ssh-relay-deploy-helpers.ts` connects transport writes and settlement callbacks to `channel.stdin.write`, and registers its `drain` event. A producer that keeps at least one queued entry behind repeated backpressure/drain cycles reaches the retained-prefix state. The proof exercises both a controlled callback/drain port and a real Node `Writable` with a deferred write callback and a synthetic 16 KiB high-water mark. No SSH connection, app, window or remote process is launched. + +Selecting an entry transfers scheduler custody to the writer's in-flight set. Clearing its consumed queue slot does not complete the write. A separate control disposes with an in-flight and a queued write: their existing results remain `unverifiable` and `refused`, respectively. The native callback can still retain the in-flight buffer until that callback reference is released. Late and duplicate callbacks do not settle it twice. + +## Results + +Both captured runtimes produce the same counts: Node 26.6 and Electron 43.7 / Node 24.21. Each runs six scenarios before and after the fix. + +| Scenario at the controlled pause | Original | Fixed | +| ----------------------------------------------------------------------- | ---------: | ------: | +| Ordinary lane: completed buffers and settlement callbacks retained | 2,048 each | 0 | +| Control lane: completed buffers and settlement callbacks retained | 2,048 each | 0 | +| Physical queue slots after those selections | 2,050 | 2 | +| Logical queued ordinary frames / bytes | 2 / 706 | 2 / 706 | +| Logical queued control frames / bytes | 2 / 184 | 2 / 184 | +| Real writable: retained written buffers, including one in flight | 128 | 1 | +| In-flight buffer retained after disposal while native callback is owned | 1 | 1 | +| Written buffers retained after complete drain or final callback release | 0 | 0 | + +The real-writable scenario completes 128 writes, starts the 129th and keeps two further writes queued. The first write preceded the rolling backlog and is collectible in both variants; the original therefore retains 127 completed buffers plus the in-flight one. Its logical budget is three frames / 49,440 bytes in both variants. Empty slots below the compaction threshold are expected and do not retain those buffers. + +The scenarios assert FIFO order, isolation of ordinary/control counters, full-drain and disposal cleanup. Five permanent lifetime regressions plus 26 existing tests pass. The original-source overlay fails the four rolling-backlog regressions and passes the other 27 tests. Existing tests cover control priority and starvation prevention, liveness bypass, synchronous drain, callback errors and duplicates, overflow, disposal, timeouts and slow-but-live transport handling. See `validation.json` for commands and full-publication quality checks. + +## Reproduce + +From the repository root after dependency installation: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/ssh-writer-consumed-prefix/reproduce.cjs +``` + +For Electron, run the installed Electron executable with `ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`, passing the same flags and script path. This is a Node-mode process with no UI. The runner has a 20-second deadline. + +An optional final argument selects the report destination, for example `notes/ssh-writer-consumed-prefix/reviewer-node.json`. Without it, the runner refreshes the corresponding artifact `node-results.json` or `electron-results.json`. + +`sources.cjs` reverses `fix.patch` in memory and verifies both original and fixed SHA-256 hashes. It also verifies all nine bundled source dependencies against the recorded hashes. No source file is rewritten. A synthetic CRLF read of the patch and source must reproduce identical canonical LF sources and hashes. `before.config.mjs` uses the same source loader for the original-source test overlay. + +## Source compatibility and scope + +`source-versions.json` records the exact scheduler baseline at the pre-fix audit commit, independent main `291b4ddd6f1c1af480169885e0fda7f9c78ff053`, and v1.4.198 `e0826956fcfc532f5a1e55b5e081f2e57e553c43`. These scheduler sources are byte-identical after LF normalization, so the same patch yields the same fixed hash. All nine bundled sources and three additional caller sources match independent main. The projected main change has no dependency on the other memory-audit fixes. + +The v1.4.198 scheduler is identical, and its writer contains the same enqueue/select/release path. Seven of the twelve dependency/caller files differ from current source; this artifact does not claim to execute the packaged historical release. + +Inputs and timing are controlled fixtures. Reachability counts establish the code-level retention mechanism; they do not measure affected-host RSS, model a reported growth rate, or establish that an incident had a continuously nonempty SSH write lane. Active pending writes, transport-owned callbacks and retained primitive sequence timestamps remain governed by their existing limits and lifecycles. diff --git a/docs/audits/ssh-writer-consumed-prefix/before.config.mjs b/docs/audits/ssh-writer-consumed-prefix/before.config.mjs new file mode 100644 index 00000000000..7fc8034a383 --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/before.config.mjs @@ -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/ssh-writer-consumed-prefix/sources.cjs') +) +const { before } = loadSources() +export default mergeConfig( + baseConfig, + defineConfig({ + plugins: [ + { + name: 'ssh-scheduler-before-fix', + enforce: 'pre', + transform(_code, id) { + const source = before.get(resolve(id.split('?')[0])) + return source === undefined ? undefined : { code: source, map: null } + } + } + ] + }) +) diff --git a/docs/audits/ssh-writer-consumed-prefix/electron-results.json b/docs/audits/ssh-writer-consumed-prefix/electron-results.json new file mode 100644 index 00000000000..c067efd1211 --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/electron-results.json @@ -0,0 +1,382 @@ +{ + "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" + }, + "sourceHashLineEndings": "canonical LF", + "crlfLoaderControl": { + "syntheticCrlfReads": 2, + "identicalSourcesAndHashes": true + }, + "cases": [ + { + "fixed": false, + "release": "drain", + "laneName": "ordinary", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 2048, + "completedCallbacksAlive": 2048, + "logicalFrames": 2, + "logicalBytes": 706, + "physicalSlots": 2050, + "head": 2048, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": false + } + }, + { + "fixed": false, + "release": "dispose", + "laneName": "ordinary", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 2048, + "completedCallbacksAlive": 2048, + "logicalFrames": 2, + "logicalBytes": 706, + "physicalSlots": 2050, + "head": 2048, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": true + } + }, + { + "fixed": false, + "release": "drain", + "laneName": "control", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 2048, + "completedCallbacksAlive": 2048, + "logicalFrames": 2, + "logicalBytes": 184, + "physicalSlots": 2050, + "head": 2048, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": false + } + }, + { + "fixed": false, + "release": "dispose", + "laneName": "control", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 2048, + "completedCallbacksAlive": 2048, + "logicalFrames": 2, + "logicalBytes": 184, + "physicalSlots": 2050, + "head": 2048, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": true + } + }, + { + "kind": "real-node-writable", + "fixed": false, + "writes": 129, + "accepted": 128, + "logicalFrames": 3, + "logicalBytes": 49440, + "physicalSlots": 130, + "retainedBuffers": 128 + }, + { + "kind": "in-flight-callback-ownership", + "fixed": false, + "retainedByNativeCallback": 1, + "afterNativeCallbackRelease": 0, + "pendingResult": { + "outcome": "unverifiable", + "reason": "transport_settlement_lost", + "bytesHandedToTransport": true + }, + "queuedResult": { + "outcome": "refused", + "reason": "transport_rejected_before_handoff" + } + }, + { + "fixed": true, + "release": "drain", + "laneName": "ordinary", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 0, + "completedCallbacksAlive": 0, + "logicalFrames": 2, + "logicalBytes": 706, + "physicalSlots": 2, + "head": 0, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": false + } + }, + { + "fixed": true, + "release": "dispose", + "laneName": "ordinary", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 0, + "completedCallbacksAlive": 0, + "logicalFrames": 2, + "logicalBytes": 706, + "physicalSlots": 2, + "head": 0, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": true + } + }, + { + "fixed": true, + "release": "drain", + "laneName": "control", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 0, + "completedCallbacksAlive": 0, + "logicalFrames": 2, + "logicalBytes": 184, + "physicalSlots": 2, + "head": 0, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": false + } + }, + { + "fixed": true, + "release": "dispose", + "laneName": "control", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 0, + "completedCallbacksAlive": 0, + "logicalFrames": 2, + "logicalBytes": 184, + "physicalSlots": 2, + "head": 0, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": true + } + }, + { + "kind": "real-node-writable", + "fixed": true, + "writes": 129, + "accepted": 128, + "logicalFrames": 3, + "logicalBytes": 49440, + "physicalSlots": 130, + "retainedBuffers": 1 + }, + { + "kind": "in-flight-callback-ownership", + "fixed": true, + "retainedByNativeCallback": 1, + "afterNativeCallbackRelease": 0, + "pendingResult": { + "outcome": "unverifiable", + "reason": "transport_settlement_lost", + "bytesHandedToTransport": true + }, + "queuedResult": { + "outcome": "refused", + "reason": "transport_rejected_before_handoff" + } + } + ], + "versions": [ + { + "fixed": false, + "sourceHashes": { + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": { + "before": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "after": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc" + } + }, + "bundleSha256": "aafe581371b67c00d50d96a0857257bee20fa317c2d8ce4ea1800e558c7a1b91", + "dependencies": [ + { + "path": "src/shared/relay-frame-decoder-contract.ts", + "sha256": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8" + }, + { + "path": "src/shared/relay-frame-buffer.ts", + "sha256": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be" + }, + { + "path": "src/shared/relay-frame-decoder.ts", + "sha256": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194" + }, + { + "path": "src/main/ssh/relay-protocol.ts", + "sha256": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34" + }, + { + "path": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "sha256": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50" + }, + { + "path": "src/shared/pty-write-settlement.ts", + "sha256": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6" + }, + { + "path": "src/main/ssh/ssh-multiplexer-transport-writer.ts", + "sha256": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071" + }, + { + "path": "src/main/ssh/ssh-channel-multiplexer.ts", + "sha256": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108" + }, + { + "path": "src/main/providers/ssh-pty-write.ts", + "sha256": "357024da7dc5bf2df1dabd43f6f90146afa239d414e29f04b17f86a6351520ae" + } + ] + }, + { + "fixed": true, + "sourceHashes": { + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": { + "before": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "after": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc" + } + }, + "bundleSha256": "8dad9c7df8842e01466d24e238ed08b8cfc642fbc3f0a6867ea2ec49058d2aa8", + "dependencies": [ + { + "path": "src/shared/relay-frame-decoder-contract.ts", + "sha256": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8" + }, + { + "path": "src/shared/relay-frame-buffer.ts", + "sha256": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be" + }, + { + "path": "src/shared/relay-frame-decoder.ts", + "sha256": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194" + }, + { + "path": "src/main/ssh/relay-protocol.ts", + "sha256": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34" + }, + { + "path": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "sha256": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc" + }, + { + "path": "src/shared/pty-write-settlement.ts", + "sha256": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6" + }, + { + "path": "src/main/ssh/ssh-multiplexer-transport-writer.ts", + "sha256": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071" + }, + { + "path": "src/main/ssh/ssh-channel-multiplexer.ts", + "sha256": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108" + }, + { + "path": "src/main/providers/ssh-pty-write.ts", + "sha256": "357024da7dc5bf2df1dabd43f6f90146afa239d414e29f04b17f86a6351520ae" + } + ] + } + ] +} diff --git a/docs/audits/ssh-writer-consumed-prefix/fix.patch b/docs/audits/ssh-writer-consumed-prefix/fix.patch new file mode 100644 index 00000000000..90244923c94 --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/fix.patch @@ -0,0 +1,18 @@ +--- a/src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts ++++ b/src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts +@@ -6 +6 @@ +- entries: T[] ++ entries: (T | undefined)[] +@@ -22,0 +23 @@ ++ queue.entries[queue.head] = undefined +@@ -24,2 +25,5 @@ +- if (queue.head === queue.entries.length) { +- queue.entries.length = 0 ++ if ( ++ queue.head === queue.entries.length || ++ (queue.head >= 1024 && queue.head * 2 >= queue.entries.length) ++ ) { ++ queue.entries = queue.entries.slice(queue.head) +@@ -32 +36 @@ +- const entries = queue.entries.slice(queue.head) ++ const entries = queue.entries.slice(queue.head).filter((entry): entry is T => entry !== undefined) diff --git a/docs/audits/ssh-writer-consumed-prefix/node-results.json b/docs/audits/ssh-writer-consumed-prefix/node-results.json new file mode 100644 index 00000000000..ef533d6c384 --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/node-results.json @@ -0,0 +1,381 @@ +{ + "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" + }, + "sourceHashLineEndings": "canonical LF", + "crlfLoaderControl": { + "syntheticCrlfReads": 2, + "identicalSourcesAndHashes": true + }, + "cases": [ + { + "fixed": false, + "release": "drain", + "laneName": "ordinary", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 2048, + "completedCallbacksAlive": 2048, + "logicalFrames": 2, + "logicalBytes": 706, + "physicalSlots": 2050, + "head": 2048, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": false + } + }, + { + "fixed": false, + "release": "dispose", + "laneName": "ordinary", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 2048, + "completedCallbacksAlive": 2048, + "logicalFrames": 2, + "logicalBytes": 706, + "physicalSlots": 2050, + "head": 2048, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": true + } + }, + { + "fixed": false, + "release": "drain", + "laneName": "control", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 2048, + "completedCallbacksAlive": 2048, + "logicalFrames": 2, + "logicalBytes": 184, + "physicalSlots": 2050, + "head": 2048, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": false + } + }, + { + "fixed": false, + "release": "dispose", + "laneName": "control", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 2048, + "completedCallbacksAlive": 2048, + "logicalFrames": 2, + "logicalBytes": 184, + "physicalSlots": 2050, + "head": 2048, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": true + } + }, + { + "kind": "real-node-writable", + "fixed": false, + "writes": 129, + "accepted": 128, + "logicalFrames": 3, + "logicalBytes": 49440, + "physicalSlots": 130, + "retainedBuffers": 128 + }, + { + "kind": "in-flight-callback-ownership", + "fixed": false, + "retainedByNativeCallback": 1, + "afterNativeCallbackRelease": 0, + "pendingResult": { + "outcome": "unverifiable", + "reason": "transport_settlement_lost", + "bytesHandedToTransport": true + }, + "queuedResult": { + "outcome": "refused", + "reason": "transport_rejected_before_handoff" + } + }, + { + "fixed": true, + "release": "drain", + "laneName": "ordinary", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 0, + "completedCallbacksAlive": 0, + "logicalFrames": 2, + "logicalBytes": 706, + "physicalSlots": 2, + "head": 0, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": false + } + }, + { + "fixed": true, + "release": "dispose", + "laneName": "ordinary", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 0, + "completedCallbacksAlive": 0, + "logicalFrames": 2, + "logicalBytes": 706, + "physicalSlots": 2, + "head": 0, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": true + } + }, + { + "fixed": true, + "release": "drain", + "laneName": "control", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 0, + "completedCallbacksAlive": 0, + "logicalFrames": 2, + "logicalBytes": 184, + "physicalSlots": 2, + "head": 0, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": false + } + }, + { + "fixed": true, + "release": "dispose", + "laneName": "control", + "during": { + "completedWrites": 2049, + "accepted": 2049, + "completedBuffersAlive": 0, + "completedCallbacksAlive": 0, + "logicalFrames": 2, + "logicalBytes": 184, + "physicalSlots": 2, + "head": 0, + "liveQueued": 2, + "disposed": false + }, + "after": { + "completedBuffersAlive": 0, + "physicalSlots": 0, + "logicalFrames": 0, + "logicalBytes": 0, + "disposed": true + } + }, + { + "kind": "real-node-writable", + "fixed": true, + "writes": 129, + "accepted": 128, + "logicalFrames": 3, + "logicalBytes": 49440, + "physicalSlots": 130, + "retainedBuffers": 1 + }, + { + "kind": "in-flight-callback-ownership", + "fixed": true, + "retainedByNativeCallback": 1, + "afterNativeCallbackRelease": 0, + "pendingResult": { + "outcome": "unverifiable", + "reason": "transport_settlement_lost", + "bytesHandedToTransport": true + }, + "queuedResult": { + "outcome": "refused", + "reason": "transport_rejected_before_handoff" + } + } + ], + "versions": [ + { + "fixed": false, + "sourceHashes": { + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": { + "before": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "after": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc" + } + }, + "bundleSha256": "aafe581371b67c00d50d96a0857257bee20fa317c2d8ce4ea1800e558c7a1b91", + "dependencies": [ + { + "path": "src/shared/relay-frame-decoder-contract.ts", + "sha256": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8" + }, + { + "path": "src/shared/relay-frame-buffer.ts", + "sha256": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be" + }, + { + "path": "src/shared/relay-frame-decoder.ts", + "sha256": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194" + }, + { + "path": "src/main/ssh/relay-protocol.ts", + "sha256": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34" + }, + { + "path": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "sha256": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50" + }, + { + "path": "src/shared/pty-write-settlement.ts", + "sha256": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6" + }, + { + "path": "src/main/ssh/ssh-multiplexer-transport-writer.ts", + "sha256": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071" + }, + { + "path": "src/main/ssh/ssh-channel-multiplexer.ts", + "sha256": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108" + }, + { + "path": "src/main/providers/ssh-pty-write.ts", + "sha256": "357024da7dc5bf2df1dabd43f6f90146afa239d414e29f04b17f86a6351520ae" + } + ] + }, + { + "fixed": true, + "sourceHashes": { + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": { + "before": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "after": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc" + } + }, + "bundleSha256": "8dad9c7df8842e01466d24e238ed08b8cfc642fbc3f0a6867ea2ec49058d2aa8", + "dependencies": [ + { + "path": "src/shared/relay-frame-decoder-contract.ts", + "sha256": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8" + }, + { + "path": "src/shared/relay-frame-buffer.ts", + "sha256": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be" + }, + { + "path": "src/shared/relay-frame-decoder.ts", + "sha256": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194" + }, + { + "path": "src/main/ssh/relay-protocol.ts", + "sha256": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34" + }, + { + "path": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "sha256": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc" + }, + { + "path": "src/shared/pty-write-settlement.ts", + "sha256": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6" + }, + { + "path": "src/main/ssh/ssh-multiplexer-transport-writer.ts", + "sha256": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071" + }, + { + "path": "src/main/ssh/ssh-channel-multiplexer.ts", + "sha256": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108" + }, + { + "path": "src/main/providers/ssh-pty-write.ts", + "sha256": "357024da7dc5bf2df1dabd43f6f90146afa239d414e29f04b17f86a6351520ae" + } + ] + } + ] +} diff --git a/docs/audits/ssh-writer-consumed-prefix/reproduce.cjs b/docs/audits/ssh-writer-consumed-prefix/reproduce.cjs new file mode 100644 index 00000000000..5cb0af998da --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/reproduce.cjs @@ -0,0 +1,57 @@ +const assert = require('node:assert/strict') +const { readFileSync, writeFileSync } = require('node:fs') +const path = require('node:path') +const { load, loadSources, canonicalLf } = require('./sources.cjs') +const { scenario, realWritableScenario, inFlightOwnership } = require('./scenario.cjs') +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function') +function checkCrlfLoader() { + const normal = loadSources() + let syntheticCrlfReads = 0 + const crlf = loadSources({ + readText(file) { + syntheticCrlfReads++ + return canonicalLf(readFileSync(file, 'utf8')).replace(/\n/g, '\r\n') + } + }) + assert.equal(syntheticCrlfReads, 2) + assert.deepEqual(crlf.before, normal.before) + assert.deepEqual(crlf.after, normal.after) + assert.deepEqual(crlf.hashes, normal.hashes) + return { syntheticCrlfReads, identicalSourcesAndHashes: true } +} +async function main() { + const timer = setTimeout(() => { + process.stderr.write('deadline\n') + process.exit(2) + }, 20_000) + const output = { + runtime: process.versions, + sourceHashLineEndings: 'canonical LF', + crlfLoaderControl: checkCrlfLoader(), + cases: [], + versions: [] + } + for (const fixed of [false, true]) { + const api = await load(fixed) + output.versions.push({ fixed, ...api.versions }) + for (const lane of ['ordinary', 'control']) { + for (const release of ['drain', 'dispose']) { + output.cases.push(await scenario(api, fixed, release, lane)) + } + } + output.cases.push(await realWritableScenario(api, fixed)) + output.cases.push(await inFlightOwnership(api, fixed)) + } + clearTimeout(timer) + const defaultName = process.versions.electron ? 'electron-results.json' : 'node-results.json' + const destination = process.argv[2] + ? path.resolve(process.argv[2]) + : path.join(__dirname, defaultName) + writeFileSync(destination, `${JSON.stringify(output, null, 2)}\n`) + process.stdout.write(`${JSON.stringify(output.cases, null, 2)}\n`) +} +main().catch((error) => { + process.stderr.write(`${error.stack}\n`) + process.exit(1) +}) diff --git a/docs/audits/ssh-writer-consumed-prefix/scenario.cjs b/docs/audits/ssh-writer-consumed-prefix/scenario.cjs new file mode 100644 index 00000000000..c43d2c4087c --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/scenario.cjs @@ -0,0 +1,260 @@ +const assert = require('node:assert/strict') +const { Writable } = require('node:stream') +const pause = () => new Promise((resolve) => setImmediate(resolve)) +async function collect() { + for (let i = 0; i < 8; i++) { + await pause() + global.gc() + } + await pause() +} +async function scenario(api, fixed, release, laneName) { + let drain, + current, + writes = 0, + next = 0, + accepted = 0 + const weak = [], + callbacks = [] + const mux = new api.SshChannelMultiplexer({ + supportsWriteSettlement: true, + write(bytes, settle) { + assert.equal(current, undefined) + const msg = JSON.parse(bytes.subarray(13).toString()) + assert.equal( + laneName === 'ordinary' ? Number.parseInt(msg.params.data, 10) : msg.params.seq, + writes + ) + weak.push(new WeakRef(bytes)) + callbacks.push(new WeakRef([...mux.writer.inFlight][0].onSettled)) + current = settle + writes++ + return false + }, + onDrain(fn) { + drain = fn + return () => { + drain = undefined + } + }, + onData() {}, + onClose() {}, + pauseReads() {}, + resumeReads() {}, + close() {} + }) + const enqueue = () => { + const id = next++ + if (laneName === 'ordinary') { + const promise = api.writeToSshPtyWithSettlement( + mux, + 'synthetic-pty', + `${id}:${'x'.repeat(256)}` + ) + void promise.then((result) => { + if (result.outcome === 'accepted') { + accepted++ + } + }) + } else { + mux.notify('git.responseAck', { streamId: 1, seq: id }) + } + } + const settle = () => { + assert.ok(current) + const cb = current + current = undefined + cb({ ok: true }) + if (laneName === 'control') { + accepted++ + } + } + enqueue() + enqueue() + enqueue() + settle() + for (let i = 0; i < 2048; i++) { + drain() + settle() + enqueue() + } + await collect() + const lane = mux.writer.scheduler[laneName] + const completedAlive = weak.reduce((n, w) => n + (w.deref() !== undefined), 0) + const during = { + completedWrites: writes, + accepted, + completedBuffersAlive: completedAlive, + completedCallbacksAlive: callbacks.reduce((n, w) => n + (w.deref() !== undefined), 0), + logicalFrames: mux.writer[`${laneName}Frames`], + logicalBytes: mux.writer[`${laneName}Bytes`], + physicalSlots: lane.entries.length, + head: lane.head, + liveQueued: lane.entries.length - lane.head, + disposed: mux.isDisposed() + } + assert.equal(during.liveQueued, 2) + assert.equal(during.logicalFrames, 2) + assert.equal(during.disposed, false) + assert.equal(accepted, writes) + assert.equal(completedAlive, fixed ? 0 : 2048) + assert.equal(during.completedCallbacksAlive, fixed ? 0 : 2048) + const sibling = laneName === 'ordinary' ? 'control' : 'ordinary' + assert.equal(mux.writer.scheduler[sibling].entries.length, 0) + assert.equal(mux.writer[`${sibling}Frames`], 0) + if (release === 'drain') { + drain() + settle() + drain() + settle() + } else { + mux.dispose() + } + await collect() + const after = { + completedBuffersAlive: weak.reduce((n, w) => n + (w.deref() !== undefined), 0), + physicalSlots: lane.entries.length, + logicalFrames: mux.writer[`${laneName}Frames`], + logicalBytes: mux.writer[`${laneName}Bytes`], + disposed: mux.isDisposed() + } + assert.equal(after.completedBuffersAlive, 0) + assert.equal(after.physicalSlots, 0) + assert.equal(after.logicalFrames, 0) + mux.dispose() + return { fixed, release, laneName, during, after } +} + +async function realWritableScenario(api, fixed) { + let complete, + writes = 0, + accepted = 0 + const weak = [] + const sink = new Writable({ + highWaterMark: 16 * 1024, + write(bytes, encoding, callback) { + assert.equal(complete, undefined) + assert.equal( + Number.parseInt(JSON.parse(bytes.subarray(13).toString()).params.data, 10), + writes + ) + weak.push(new WeakRef(bytes)) + writes++ + complete = callback + } + }) + const mux = new api.SshChannelMultiplexer({ + supportsWriteSettlement: true, + write: (bytes, onSettled) => + sink.write(bytes, (error) => onSettled(error ? { ok: false, error } : { ok: true })), + onDrain: (fn) => { + sink.on('drain', fn) + return () => sink.off('drain', fn) + }, + onData() {}, + onClose() {}, + close() {} + }) + let next = 0 + const enqueue = () => + void api + .writeToSshPtyWithSettlement(mux, 'synthetic-pty', `${next++}:${'x'.repeat(16 * 1024)}`) + .then((result) => { + if (result.outcome === 'accepted') { + accepted++ + } + }) + const completeWrite = async () => { + assert.ok(complete) + const cb = complete + complete = undefined + cb() + await pause() + } + enqueue() + enqueue() + enqueue() + for (let i = 0; i < 128; i++) { + await completeWrite() + enqueue() + } + await collect() + const retained = weak.reduce((n, w) => n + (w.deref() !== undefined), 0) + const result = { + kind: 'real-node-writable', + fixed, + writes, + accepted, + logicalFrames: mux.writer.ordinaryFrames, + logicalBytes: mux.writer.ordinaryBytes, + physicalSlots: mux.writer.scheduler.ordinary.entries.length, + retainedBuffers: retained + } + assert.equal(accepted, 128) + assert.equal(writes, 129) + assert.equal(result.logicalFrames, 3) + assert.equal(retained, fixed ? 1 : 128) + while (complete) { + await completeWrite() + } + await collect() + assert.equal( + weak.reduce((n, w) => n + (w.deref() !== undefined), 0), + 0 + ) + assert.equal(mux.writer.scheduler.ordinary.entries.length, 0) + assert.equal(mux.writer.ordinaryFrames, 0) + mux.dispose() + sink.destroy() + return result +} +async function inFlightOwnership(api, fixed) { + let current, drain + const weak = [] + const mux = new api.SshChannelMultiplexer({ + supportsWriteSettlement: true, + write(bytes, fn) { + weak.push(new WeakRef(bytes)) + current = fn + return false + }, + onDrain(fn) { + drain = fn + return () => { + drain = undefined + } + }, + onData() {}, + onClose() {}, + close() {} + }) + const pending = api.writeToSshPtyWithSettlement(mux, 'synthetic-pty', 'in-flight') + const queued = api.writeToSshPtyWithSettlement(mux, 'synthetic-pty', 'queued') + mux.dispose() + const pendingResult = await pending, + queuedResult = await queued + assert.equal(pendingResult.outcome, 'unverifiable') + assert.equal(queuedResult.outcome, 'refused') + await collect() + const retainedByNativeCallback = weak.reduce((n, w) => n + (w.deref() !== undefined), 0) + assert.equal(retainedByNativeCallback, 1) + assert.equal(drain, undefined) + current({ ok: true }) + current({ ok: false, error: new Error('synthetic late duplicate') }) + current = undefined + await collect() + assert.equal( + weak.reduce((n, w) => n + (w.deref() !== undefined), 0), + 0 + ) + return { + kind: 'in-flight-callback-ownership', + fixed, + retainedByNativeCallback, + afterNativeCallbackRelease: 0, + pendingResult, + queuedResult + } +} + +module.exports = { scenario, realWritableScenario, inFlightOwnership } diff --git a/docs/audits/ssh-writer-consumed-prefix/source-versions.json b/docs/audits/ssh-writer-consumed-prefix/source-versions.json new file mode 100644 index 00000000000..7dc96bfd77d --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/source-versions.json @@ -0,0 +1,208 @@ +{ + "baselineHashes": { + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50" + }, + "fixedHashes": { + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc" + }, + "sourceHashLineEndings": "canonical LF", + "namedRefs": { + "2e83de3154c4ee1bbeea816734b892c34500a5cc": { + "path": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "sha256": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "equalsBaseline": true + }, + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "path": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "sha256": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "equalsBaseline": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "path": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "sha256": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "equalsBaseline": true + } + }, + "provenance": [ + { + "path": "src/shared/relay-frame-decoder-contract.ts", + "currentBaselineSha256": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "identical": true + } + }, + "bundled": true + }, + { + "path": "src/shared/relay-frame-buffer.ts", + "currentBaselineSha256": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "4d40ca7cb812e0af6edf78340b017a150ce9529b71396e4c946358f4ed698f81", + "identical": false + } + }, + "bundled": true + }, + { + "path": "src/shared/relay-frame-decoder.ts", + "currentBaselineSha256": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "identical": true + } + }, + "bundled": true + }, + { + "path": "src/main/ssh/relay-protocol.ts", + "currentBaselineSha256": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "identical": true + } + }, + "bundled": true + }, + { + "path": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "currentBaselineSha256": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "identical": true + } + }, + "bundled": true + }, + { + "path": "src/shared/pty-write-settlement.ts", + "currentBaselineSha256": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": null, + "identical": false + } + }, + "bundled": true + }, + { + "path": "src/main/ssh/ssh-multiplexer-transport-writer.ts", + "currentBaselineSha256": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "4a73e2194f15ec0604802fe6810742f34930fc55e542458b6d8ab7344ee841a2", + "identical": false + } + }, + "bundled": true + }, + { + "path": "src/main/ssh/ssh-channel-multiplexer.ts", + "currentBaselineSha256": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "480c722b27dd1ffb8c70bfca8fb3568294ff2777b7b02607548df93bb280f6ae", + "identical": false + } + }, + "bundled": true + }, + { + "path": "src/main/providers/ssh-pty-write.ts", + "currentBaselineSha256": "357024da7dc5bf2df1dabd43f6f90146afa239d414e29f04b17f86a6351520ae", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "357024da7dc5bf2df1dabd43f6f90146afa239d414e29f04b17f86a6351520ae", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "76c4994509a0de61052ceb984a8a9fa0f958bd6a87239ea8e95ff7702f242c67", + "identical": false + } + }, + "bundled": true + }, + { + "path": "src/main/ssh/ssh-relay-deploy-helpers.ts", + "currentBaselineSha256": "5452b8a441268a42abe09ae71ae64c5684e070a7469eed0b421ab4cd8e41abae", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "5452b8a441268a42abe09ae71ae64c5684e070a7469eed0b421ab4cd8e41abae", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "d9f42be19fb6c1a7921d2f0258f4b4dea814e0dd19f228616d1a2c4158a7c41e", + "identical": false + } + }, + "bundled": false + }, + { + "path": "src/main/ssh/ssh-git-response-stream-reader.ts", + "currentBaselineSha256": "8e3a2a5606f95ce1c70c1397ca5a807d90989074ea2be41497785c269381af0e", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "8e3a2a5606f95ce1c70c1397ca5a807d90989074ea2be41497785c269381af0e", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "8e3a2a5606f95ce1c70c1397ca5a807d90989074ea2be41497785c269381af0e", + "identical": true + } + }, + "bundled": false + }, + { + "path": "src/main/providers/ssh-pty-provider.ts", + "currentBaselineSha256": "7bf1a9e41b606dcfd73bd2a4aa9d9aa185b12f18f9beb027b640fe10cf360123", + "namedRefs": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "sha256": "7bf1a9e41b606dcfd73bd2a4aa9d9aa185b12f18f9beb027b640fe10cf360123", + "identical": true + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "sha256": "f37a0e8688544a79b35e2d2342282f215cd80928adb9da4824d185598fb8a6b6", + "identical": false + } + }, + "bundled": false + } + ] +} diff --git a/docs/audits/ssh-writer-consumed-prefix/sources.cjs b/docs/audits/ssh-writer-consumed-prefix/sources.cjs new file mode 100644 index 00000000000..8518863c55c --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/sources.cjs @@ -0,0 +1,102 @@ +const assert = require('node:assert/strict') +const { readFileSync } = require('node:fs') +const { createHash } = require('node:crypto') +const path = require('node:path') +const Module = require('node:module') +const esbuild = require('esbuild') +const { applyPatch, parsePatch, reversePatch } = require('diff') +const { resolve } = path +const root = resolve(__dirname, '../../..') +const canonicalLf = (text) => text.replace(/\r\n/g, '\n') +const read = (file) => canonicalLf(readFileSync(file, 'utf8')) +const sha = (value) => createHash('sha256').update(value).digest('hex') +const sourcePath = 'src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts' + +function loadSources({ readText = (file) => readFileSync(file, 'utf8') } = {}) { + const root = resolve(__dirname, '../../..') + const expected = JSON.parse(readFileSync(resolve(__dirname, 'source-versions.json'), 'utf8')) + const parsed = parsePatch(canonicalLf(readText(resolve(__dirname, 'fix.patch')))) + 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 = canonicalLf(readText(absolute)) + 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}`) + assert.equal(hash(current), expected.fixedHashes[path], `Fixed source drift: ${path}`) + before.set(absolute, baseline) + after.set(absolute, current) + hashes[path] = { before: hash(baseline), after: hash(current) } + } + return { root, before, after, hashes } +} + +async function load(fixed) { + const { before, after, hashes } = loadSources() + const source = (fixed ? after : before).get(resolve(root, sourcePath)) + const built = await esbuild.build({ + stdin: { + contents: + "export { SshChannelMultiplexer } from './src/main/ssh/ssh-channel-multiplexer'; export { writeToSshPtyWithSettlement } from './src/main/providers/ssh-pty-write'", + resolveDir: root, + sourcefile: 'fixture.ts', + loader: 'ts' + }, + bundle: true, + platform: 'node', + format: 'cjs', + write: false, + metafile: true, + plugins: [ + { + name: 'scheduler-variant', + setup(builder) { + builder.onLoad({ filter: /ssh-multiplexer-writer-lane-scheduler\.ts$/ }, (args) => { + assert.equal(args.path, resolve(root, sourcePath)) + return { contents: source, loader: 'ts' } + }) + } + } + ] + }) + const filename = resolve(__dirname, 'in-memory.cjs') + const expected = JSON.parse(readFileSync(resolve(__dirname, 'source-versions.json'), 'utf8')) + const dependencies = Object.keys(built.metafile.inputs) + .filter((file) => file.startsWith('src/')) + .map((file) => ({ + path: file, + sha256: sha(file === sourcePath ? source : read(resolve(root, file))) + })) + const baselineDependencies = new Map( + expected.provenance + .filter((entry) => entry.bundled) + .map((entry) => [entry.path, entry.currentBaselineSha256]) + ) + assert.equal(dependencies.length, baselineDependencies.size) + for (const dependency of dependencies) { + const expectedHash = + fixed && dependency.path === sourcePath + ? expected.fixedHashes[sourcePath] + : baselineDependencies.get(dependency.path) + assert.equal(dependency.sha256, expectedHash, `Dependency drift: ${dependency.path}`) + } + const loaded = new Module(filename, module) + loaded.filename = filename + loaded.paths = Module._nodeModulePaths(__dirname) + loaded._compile(built.outputFiles[0].text, filename) + return { + ...loaded.exports, + versions: { + sourceHashes: hashes, + bundleSha256: sha(built.outputFiles[0].contents), + dependencies + } + } +} +module.exports = { load, loadSources, canonicalLf } diff --git a/docs/audits/ssh-writer-consumed-prefix/validation.json b/docs/audits/ssh-writer-consumed-prefix/validation.json new file mode 100644 index 00000000000..613afe5f3a9 --- /dev/null +++ b/docs/audits/ssh-writer-consumed-prefix/validation.json @@ -0,0 +1,157 @@ +{ + "tests": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-multiplexer-writer-retention.test.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts src/main/ssh/ssh-channel-multiplexer-saturation-wedge.test.ts src/main/providers/ssh-pty-write.test.ts", + "passed": 31, + "files": 5, + "newRegressionCases": 5, + "exitCode": 0 + }, + "baselineOverlay": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/ssh-writer-consumed-prefix/before.config.mjs src/main/ssh/ssh-multiplexer-writer-retention.test.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts src/main/ssh/ssh-channel-multiplexer-saturation-wedge.test.ts src/main/providers/ssh-pty-write.test.ts", + "passed": 27, + "expectedFailed": 4, + "failures": "Ordinary/control rolling backlog, each with drain/dispose cleanup variant: 32 completed buffers remain reachable.", + "exitCode": 1 + }, + "typecheck": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm tc:node", + "exitCode": 0 + }, + "publicationQuality": { + "files": [ + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "src/main/ssh/ssh-multiplexer-writer-retention.test.ts", + "docs/audits/ssh-writer-consumed-prefix/reproduce.cjs", + "docs/audits/ssh-writer-consumed-prefix/sources.cjs", + "docs/audits/ssh-writer-consumed-prefix/scenario.cjs", + "docs/audits/ssh-writer-consumed-prefix/before.config.mjs" + ], + "scans": [ + { + "label": "code quality", + "argv": [ + "pnpm", + "exec", + "oxlint", + "--no-ignore", + "--deny-warnings", + "--report-unused-disable-directives-severity", + "warn", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "src/main/ssh/ssh-multiplexer-writer-retention.test.ts", + "docs/audits/ssh-writer-consumed-prefix/reproduce.cjs", + "docs/audits/ssh-writer-consumed-prefix/sources.cjs", + "docs/audits/ssh-writer-consumed-prefix/scenario.cjs", + "docs/audits/ssh-writer-consumed-prefix/before.config.mjs" + ], + "exitCode": 0 + }, + { + "label": "casting code quality", + "argv": [ + "pnpm", + "exec", + "oxlint", + "--no-ignore", + "--deny-warnings", + "--config", + "config/oxlint-code-quality-casting.json", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "src/main/ssh/ssh-multiplexer-writer-retention.test.ts", + "docs/audits/ssh-writer-consumed-prefix/reproduce.cjs", + "docs/audits/ssh-writer-consumed-prefix/sources.cjs", + "docs/audits/ssh-writer-consumed-prefix/scenario.cjs", + "docs/audits/ssh-writer-consumed-prefix/before.config.mjs" + ], + "exitCode": 0 + }, + { + "label": "type-aware code quality", + "argv": [ + "pnpm", + "exec", + "oxlint", + "--no-ignore", + "--deny-warnings", + "--type-aware", + "--config", + "config/oxlint-code-quality-type-aware.json", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "src/main/ssh/ssh-multiplexer-writer-retention.test.ts", + "docs/audits/ssh-writer-consumed-prefix/reproduce.cjs", + "docs/audits/ssh-writer-consumed-prefix/sources.cjs", + "docs/audits/ssh-writer-consumed-prefix/scenario.cjs", + "docs/audits/ssh-writer-consumed-prefix/before.config.mjs" + ], + "exitCode": 0 + }, + { + "label": "React Doctor", + "argv": [ + "pnpm", + "exec", + "oxlint", + "--no-ignore", + "--deny-warnings", + "--config", + "config/oxlint-react-doctor.json", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "src/main/ssh/ssh-multiplexer-writer-retention.test.ts", + "docs/audits/ssh-writer-consumed-prefix/reproduce.cjs", + "docs/audits/ssh-writer-consumed-prefix/sources.cjs", + "docs/audits/ssh-writer-consumed-prefix/scenario.cjs", + "docs/audits/ssh-writer-consumed-prefix/before.config.mjs" + ], + "exitCode": 0 + }, + { + "label": "design system", + "argv": [ + "pnpm", + "exec", + "oxlint", + "--no-ignore", + "--deny-warnings", + "--config", + "config/oxlint-design-system.json", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "src/main/ssh/ssh-multiplexer-writer-retention.test.ts", + "docs/audits/ssh-writer-consumed-prefix/reproduce.cjs", + "docs/audits/ssh-writer-consumed-prefix/sources.cjs", + "docs/audits/ssh-writer-consumed-prefix/scenario.cjs", + "docs/audits/ssh-writer-consumed-prefix/before.config.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 above include every durable CJS/MJS file; the ordinary changed gate cannot see newly ignored artifacts before staging." + }, + "proofs": { + "node": "ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/ssh-writer-consumed-prefix/reproduce.cjs", + "electron": "Installed Electron executable with ELECTRON_RUN_AS_NODE=1 and ORCA_BACKGROUND_LAUNCH=1; same flags and script path.", + "exitCodes": [0, 0], + "beforeAfterCasesPerRuntime": 12, + "crlfLoaderControl": true, + "bundledSourceHashChecks": 9 + }, + "sourceParity": { + "identicalNamedSchedulerBaselines": 3, + "independentMainIdenticalBundledAndCallerSources": 12, + "historicalIdenticalBundledAndCallerSources": 5, + "historicalProvenanceSources": 12, + "historicalExecutableReplay": false + }, + "format": "All 12 promoted TS/CJS/MJS/MD/JSON paths checked with oxfmt stdin mode, excluding fix.patch.", + "gitDiffCheckExitCode": 0, + "publicationWhitespace": { + "commandTemplate": "git diff --no-index --check ", + "files": 12, + "expectedExitCode": 1, + "diagnostics": 0, + "note": "Each complete file is checked, including ignored new artifacts. Exit 1 only reports its content differs from an empty file; no whitespace diagnostics. fix.patch uses zero-context hunks." + } +} diff --git a/src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts b/src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts index 74ad5d593b6..98a00768f5a 100644 --- a/src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts +++ b/src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts @@ -3,7 +3,7 @@ import type { MultiplexerWriterLane } from './ssh-multiplexer-transport-writer' const CONTROL_WRITES_BEFORE_ORDINARY = 4 type LaneQueue = { - entries: T[] + entries: (T | undefined)[] head: number } @@ -20,16 +20,20 @@ function shift(queue: LaneQueue): T | undefined { if (entry === undefined) { return undefined } + queue.entries[queue.head] = undefined queue.head += 1 - if (queue.head === queue.entries.length) { - queue.entries.length = 0 + if ( + queue.head === queue.entries.length || + (queue.head >= 1024 && queue.head * 2 >= queue.entries.length) + ) { + queue.entries = queue.entries.slice(queue.head) queue.head = 0 } return entry } function clear(queue: LaneQueue): T[] { - const entries = queue.entries.slice(queue.head) + const entries = queue.entries.slice(queue.head).filter((entry): entry is T => entry !== undefined) queue.entries.length = 0 queue.head = 0 return entries diff --git a/src/main/ssh/ssh-multiplexer-writer-retention.test.ts b/src/main/ssh/ssh-multiplexer-writer-retention.test.ts new file mode 100644 index 00000000000..9c74fd8c734 --- /dev/null +++ b/src/main/ssh/ssh-multiplexer-writer-retention.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it } from 'vitest' +import { + SshMultiplexerTransportWriter, + type MultiplexerTransportWriteResult, + type MultiplexerWriterLane +} from './ssh-multiplexer-transport-writer' + +async function collect(): Promise { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + for (let round = 0; round < 6; round += 1) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } +} + +function harness() { + let drain: (() => void) | undefined + let nativeCallback: ((result: MultiplexerTransportWriteResult) => void) | undefined + const buffers: WeakRef[] = [] + const receipts: WeakRef<{ index: number }>[] = [] + const writes: number[] = [] + const settlements: { index: number; outcome: string }[] = [] + const writer = new SshMultiplexerTransportWriter( + { + supportsWriteSettlement: true, + write: (bytes, onSettled) => { + expect(nativeCallback).toBeUndefined() + nativeCallback = onSettled + writes.push(bytes.readUInt32BE()) + return false + }, + onDrain: (listener) => { + drain = listener + return () => { + drain = undefined + } + }, + onData: () => {}, + onClose: () => {} + }, + (error) => { + throw error + } + ) + return { + writer, + buffers, + receipts, + writes, + settlements, + enqueue(index: number, lane: MultiplexerWriterLane): void { + const data = Buffer.alloc(32) + data.writeUInt32BE(index) + const receipt = { index } + buffers.push(new WeakRef(data)) + receipts.push(new WeakRef(receipt)) + expect( + writer.enqueue(data, lane, (result) => { + settlements.push({ index: receipt.index, outcome: result.outcome }) + }) + ).toBe(true) + }, + drain(): void { + if (!drain) { + throw new Error('Missing drain listener') + } + drain() + }, + complete(): void { + const callback = nativeCallback + nativeCallback = undefined + if (!callback) { + throw new Error('Missing native write') + } + callback({ ok: true }) + }, + duplicateCompletion(): void { + nativeCallback?.({ ok: true }) + nativeCallback?.({ ok: false, error: new Error('Late duplicate failure') }) + } + } +} + +describe('SSH writer completed entry lifetime', () => { + for (const lane of ['ordinary', 'control'] as const) { + it.each(['drain', 'dispose'] as const)( + `releases completed ${lane} entries during a rolling backlog, then %s`, + async (release) => { + const state = harness() + try { + state.enqueue(0, lane) + state.enqueue(1, lane) + state.enqueue(2, lane) + state.complete() + for (let index = 0; index < 32; index += 1) { + state.drain() + state.complete() + state.enqueue(index + 3, lane) + } + await collect() + expect(state.buffers.slice(0, 33).filter((ref) => ref.deref())).toHaveLength(0) + expect(state.receipts.slice(0, 33).filter((ref) => ref.deref())).toHaveLength(0) + expect(state.buffers.slice(33).filter((ref) => ref.deref())).toHaveLength(2) + expect(state.receipts.slice(33).filter((ref) => ref.deref())).toHaveLength(2) + expect(state.writes).toEqual(Array.from({ length: 33 }, (_, index) => index)) + expect(state.settlements).toEqual( + state.writes.map((index) => ({ index, outcome: 'accepted' })) + ) + + if (release === 'drain') { + state.drain() + state.complete() + state.drain() + state.complete() + } else { + state.writer.dispose() + } + await collect() + expect(state.buffers.filter((ref) => ref.deref())).toHaveLength(0) + expect(state.receipts.filter((ref) => ref.deref())).toHaveLength(0) + expect(state.settlements).toHaveLength(35) + expect(state.settlements.slice(33).map((result) => result.outcome)).toEqual( + release === 'drain' ? ['accepted', 'accepted'] : ['refused', 'refused'] + ) + } finally { + state.writer.dispose() + } + } + ) + } + + it('preserves in-flight callback ownership and one settlement across disposal', async () => { + const state = harness() + try { + state.enqueue(0, 'ordinary') + state.enqueue(1, 'ordinary') + state.writer.dispose() + await collect() + expect(state.buffers[0]?.deref()).toBeDefined() + expect(state.receipts[0]?.deref()).toBeDefined() + expect(state.buffers[1]?.deref()).toBeUndefined() + expect(state.receipts[1]?.deref()).toBeUndefined() + expect(state.settlements).toEqual([ + { index: 1, outcome: 'refused' }, + { index: 0, outcome: 'unverifiable' } + ]) + state.duplicateCompletion() + state.complete() + await collect() + expect(state.buffers.filter((ref) => ref.deref())).toHaveLength(0) + expect(state.receipts.filter((ref) => ref.deref())).toHaveLength(0) + expect(state.settlements).toHaveLength(2) + } finally { + state.writer.dispose() + } + }) +}) From 1d09d557878856885b3654e96b8cbb8e570f1572 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:28 -0700 Subject: [PATCH 19/59] fix: fence viewport state after browser guest retirement (#21160) Co-authored-by: m4air --- .../README.md | 44 +++ .../baseline-results.json | 94 ++++++ .../baseline-source.txt | 219 ++++++++++++ .../fix.patch | 18 + .../fixed-results.json | 260 +++++++++++++++ .../source-versions.json | 98 ++++++ .../validation.json | 36 ++ .../vitest.config.mjs | 36 ++ ...browser-manager-viewport-ownership.test.ts | 312 ++++++++++++++++++ src/main/browser/browser-manager-viewport.ts | 11 +- 10 files changed, 1127 insertions(+), 1 deletion(-) create mode 100644 docs/audits/browser-viewport-owner-retention/README.md create mode 100644 docs/audits/browser-viewport-owner-retention/baseline-results.json create mode 100644 docs/audits/browser-viewport-owner-retention/baseline-source.txt create mode 100644 docs/audits/browser-viewport-owner-retention/fix.patch create mode 100644 docs/audits/browser-viewport-owner-retention/fixed-results.json create mode 100644 docs/audits/browser-viewport-owner-retention/source-versions.json create mode 100644 docs/audits/browser-viewport-owner-retention/validation.json create mode 100644 docs/audits/browser-viewport-owner-retention/vitest.config.mjs create mode 100644 src/main/browser/browser-manager-viewport-ownership.test.ts diff --git a/docs/audits/browser-viewport-owner-retention/README.md b/docs/audits/browser-viewport-owner-retention/README.md new file mode 100644 index 00000000000..bcd64e43a9a --- /dev/null +++ b/docs/audits/browser-viewport-owner-retention/README.md @@ -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. diff --git a/docs/audits/browser-viewport-owner-retention/baseline-results.json b/docs/audits/browser-viewport-owner-retention/baseline-results.json new file mode 100644 index 00000000000..a089d27272f --- /dev/null +++ b/docs/audits/browser-viewport-owner-retention/baseline-results.json @@ -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" + ] + } + ] +} diff --git a/docs/audits/browser-viewport-owner-retention/baseline-source.txt b/docs/audits/browser-viewport-owner-retention/baseline-source.txt new file mode 100644 index 00000000000..ce31dbe37e1 --- /dev/null +++ b/docs/audits/browser-viewport-owner-retention/baseline-source.txt @@ -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 { + 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 { + // 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 { + 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 { + // 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 { + 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 + } + } +} diff --git a/docs/audits/browser-viewport-owner-retention/fix.patch b/docs/audits/browser-viewport-owner-retention/fix.patch new file mode 100644 index 00000000000..be47f36792b --- /dev/null +++ b/docs/audits/browser-viewport-owner-retention/fix.patch @@ -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 ++ ) { diff --git a/docs/audits/browser-viewport-owner-retention/fixed-results.json b/docs/audits/browser-viewport-owner-retention/fixed-results.json new file mode 100644 index 00000000000..3567f8d3eff --- /dev/null +++ b/docs/audits/browser-viewport-owner-retention/fixed-results.json @@ -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": [] + } + ] +} diff --git a/docs/audits/browser-viewport-owner-retention/source-versions.json b/docs/audits/browser-viewport-owner-retention/source-versions.json new file mode 100644 index 00000000000..dabc3a96c01 --- /dev/null +++ b/docs/audits/browser-viewport-owner-retention/source-versions.json @@ -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" + } + } + ] +} diff --git a/docs/audits/browser-viewport-owner-retention/validation.json b/docs/audits/browser-viewport-owner-retention/validation.json new file mode 100644 index 00000000000..2a7de53af18 --- /dev/null +++ b/docs/audits/browser-viewport-owner-retention/validation.json @@ -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" + ] +} diff --git a/docs/audits/browser-viewport-owner-retention/vitest.config.mjs b/docs/audits/browser-viewport-owner-retention/vitest.config.mjs new file mode 100644 index 00000000000..37cec186182 --- /dev/null +++ b/docs/audits/browser-viewport-owner-retention/vitest.config.mjs @@ -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 + } + } + ] + : [] +} diff --git a/src/main/browser/browser-manager-viewport-ownership.test.ts b/src/main/browser/browser-manager-viewport-ownership.test.ts new file mode 100644 index 00000000000..8b4492f36fd --- /dev/null +++ b/src/main/browser/browser-manager-viewport-ownership.test.ts @@ -0,0 +1,312 @@ +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + appGetPathMock: vi.fn(() => '/downloads'), + shellOpenExternalMock: vi.fn(), + browserWindowFromWebContentsMock: vi.fn(), + menuBuildFromTemplateMock: vi.fn(), + guestOffMock: vi.fn(), + guestOnMock: vi.fn(), + guestSetBackgroundThrottlingMock: vi.fn(), + guestSetWindowOpenHandlerMock: vi.fn(), + guestOpenDevToolsMock: vi.fn(), + webContentsFromIdMock: vi.fn(), + screenGetCursorScreenPointMock: vi.fn(() => ({ x: 0, y: 0 })), + openPopupWithOriginBarMock: vi.fn(), + processUserAgentMode: 'clean', + processUserAgent: '' +})) + +vi.mock('electron', () => ({ + app: { getPath: mocks.appGetPathMock }, + BrowserWindow: { fromWebContents: mocks.browserWindowFromWebContentsMock }, + clipboard: { writeText: vi.fn() }, + shell: { openExternal: mocks.shellOpenExternalMock }, + Menu: { buildFromTemplate: mocks.menuBuildFromTemplateMock }, + screen: { getCursorScreenPoint: mocks.screenGetCursorScreenPointMock }, + webContents: { fromId: mocks.webContentsFromIdMock } +})) +vi.mock('./popup-origin-bar-window', () => ({ + openPopupWithOriginBar: mocks.openPopupWithOriginBarMock +})) + +vi.mock('./browser-process-user-agent', () => ({ + getBrowserProcessUserAgentIdentity: () => ({ + mode: mocks.processUserAgentMode, + userAgent: mocks.processUserAgent + }) +})) + +import { browserManager } from './browser-manager' +import { resetBrowserManagerMocks, resetBrowserManagerState } from './browser-manager-test-harness' +import { + createViewportGuestFactory, + GUEST_CLEAN_UA, + GUEST_ELECTRON_UA +} from './browser-manager-viewport-test-fixtures' + +const makeGuest = createViewportGuestFactory(mocks) +const mobile = { width: 375, height: 667, deviceScaleFactor: 2, mobile: true } +const desktop = { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false } +const guests = new Map>() +const registeredGuests = readViewportStateMap('webContentsIdByTabId') +const uaIntents = readViewportStateMap('viewportUaOverrideMobileByTabId') +const presetIntents = readViewportStateMap('viewportPresetActiveByTabId') +const pendingOperations = readViewportStateMap('viewportOpsByTabId') + +function readViewportStateMap( + name: + | 'webContentsIdByTabId' + | 'viewportUaOverrideMobileByTabId' + | 'viewportPresetActiveByTabId' + | 'viewportOpsByTabId' +): Map { + const value: unknown = browserManager[name] + if (!(value instanceof Map)) { + throw new Error(`Expected manager state map: ${name}`) + } + return value +} + +function register(tab: string, id: number) { + const handle = makeGuest(id) + guests.set(id, handle.guest) + expect( + browserManager.registerOffscreenGuest({ + browserPageId: tab, + webContentsId: id + }) + ).toBe(true) + return handle +} + +function pause(handle: ReturnType, method: string) { + const entered = Promise.withResolvers() + const gate = Promise.withResolvers() + let blocked = false + handle.debuggerSendCommand.mockImplementation((next) => { + if (!blocked && next === method) { + blocked = true + entered.resolve() + return gate.promise + } + return Promise.resolve() + }) + return { entered: entered.promise, ...gate } +} + +describe('browser viewport operation ownership', () => { + beforeEach(() => { + expect(process.env.ORCA_BACKGROUND_LAUNCH).toBe('1') + resetBrowserManagerMocks(mocks) + resetBrowserManagerState() + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + guests.clear() + mocks.webContentsFromIdMock.mockImplementation((id) => guests.get(id)) + }) + afterEach(() => { + browserManager.unregisterAll() + vi.restoreAllMocks() + }) + + it('does not recreate closed-tab UA intent after a late touch completion', async () => { + const handle = register('closed', 100) + const gate = pause(handle, 'Emulation.setTouchEmulationEnabled') + const result = browserManager.setViewportOverride('closed', mobile) + await gate.entered + browserManager.unregisterGuest('closed') + expect(uaIntents.size).toBe(0) + const isDestroyed = handle.guest.isDestroyed + expect(vi.isMockFunction(isDestroyed)).toBe(true) + if (vi.isMockFunction(isDestroyed)) { + isDestroyed.mockReturnValue(true) + } + gate.resolve() + await expect(result).resolves.toBe(false) + expect(registeredGuests.size).toBe(0) + expect(presetIntents.size).toBe(0) + expect(uaIntents.get('closed')).toBeUndefined() + }) + + it('does not restore closed-tab UA intent after a failed clear', async () => { + const handle = register('clear-close', 101) + await expect(browserManager.setViewportOverride('clear-close', mobile)).resolves.toBe(true) + const gate = pause(handle, 'Emulation.setUserAgentOverride') + const result = browserManager.setViewportOverride('clear-close', null) + await gate.entered + browserManager.unregisterGuest('clear-close') + const isDestroyed = handle.guest.isDestroyed + expect(vi.isMockFunction(isDestroyed)).toBe(true) + if (vi.isMockFunction(isDestroyed)) { + isDestroyed.mockReturnValue(true) + } + gate.reject(new Error('Target closed')) + await expect(result).resolves.toBe(false) + expect(uaIntents.get('clear-close')).toBeUndefined() + }) + + it('preserves replacement desktop intent after an old clear fails', async () => { + const handle = register('replacement', 102) + await browserManager.setViewportOverride('replacement', mobile) + const gate = pause(handle, 'Emulation.setUserAgentOverride') + const result = browserManager.setViewportOverride('replacement', null) + await gate.entered + browserManager.unregisterGuest('replacement') + register('replacement', 103) + await expect(browserManager.setViewportOverride('replacement', desktop)).resolves.toBe(true) + expect(uaIntents.get('replacement')).toBe(false) + gate.reject(new Error('Old target closed')) + await expect(result).resolves.toBe(false) + expect(registeredGuests.get('replacement')).toBe(103) + expect(uaIntents.get('replacement')).toBe(false) + expect(presetIntents.get('replacement')).toEqual({ + guestWebContentsId: 103, + active: true + }) + }) + + it('preserves replacement mobile intent after an old clear resumes', async () => { + const handle = register('late-delete', 104) + await browserManager.setViewportOverride('late-delete', desktop) + const gate = pause(handle, 'Emulation.setTouchEmulationEnabled') + const result = browserManager.setViewportOverride('late-delete', null) + await gate.entered + browserManager.unregisterGuest('late-delete') + register('late-delete', 105) + await browserManager.setViewportOverride('late-delete', mobile) + gate.resolve() + await expect(result).resolves.toBe(false) + expect(uaIntents.has('late-delete')).toBe(true) + expect(registeredGuests.get('late-delete')).toBe(105) + }) + + it('same-owner clear failure still restores the legitimate earlier intent', async () => { + const handle = register('same-owner', 106) + await browserManager.setViewportOverride('same-owner', mobile) + const gate = pause(handle, 'Emulation.setUserAgentOverride') + const result = browserManager.setViewportOverride('same-owner', null) + await gate.entered + gate.reject(new Error('Protocol error')) + await expect(result).resolves.toBe(false) + expect(uaIntents.get('same-owner')).toBe(true) + }) + + it('old apply cannot overwrite a replacement guest desktop intent', async () => { + const old = register('late-apply', 110) + const gate = pause(old, 'Emulation.setTouchEmulationEnabled') + const pending = browserManager.setViewportOverride('late-apply', mobile) + await gate.entered + browserManager.unregisterGuest('late-apply') + register('late-apply', 111) + await browserManager.setViewportOverride('late-apply', desktop) + gate.resolve() + await expect(pending).resolves.toBe(false) + expect(uaIntents.get('late-apply')).toBe(false) + }) + + it('an old guest cannot write UA intent after replacement in native process mode', async () => { + mocks.processUserAgentMode = 'native' + mocks.processUserAgent = GUEST_ELECTRON_UA + const old = register('native-replacement', 112) + const gate = pause(old, 'Emulation.setTouchEmulationEnabled') + const pending = browserManager.setViewportOverride('native-replacement', mobile) + await gate.entered + browserManager.unregisterGuest('native-replacement') + register('native-replacement', 113) + gate.resolve() + await expect(pending).resolves.toBe(false) + expect(uaIntents.get('native-replacement')).toBeUndefined() + }) + + it('old queued operations cannot remove or join a replacement promise tail', async () => { + const old = register('queued-replacement', 114) + const oldGate = pause(old, 'Emulation.setTouchEmulationEnabled') + const first = browserManager.setViewportOverride('queued-replacement', mobile) + const second = browserManager.setViewportOverride('queued-replacement', null) + await oldGate.entered + browserManager.unregisterGuest('queued-replacement') + const replacement = register('queued-replacement', 115) + const newGate = pause(replacement, 'Emulation.setTouchEmulationEnabled') + const replacementFirst = browserManager.setViewportOverride('queued-replacement', desktop) + const replacementSecond = browserManager.setViewportOverride('queued-replacement', mobile) + await newGate.entered + const tail = pendingOperations.get('queued-replacement') + oldGate.resolve() + await expect(first).resolves.toBe(false) + await expect(second).resolves.toBe(false) + expect(pendingOperations.get('queued-replacement')).toBe(tail) + newGate.resolve() + await expect(replacementFirst).resolves.toBe(true) + await expect(replacementSecond).resolves.toBe(true) + expect(pendingOperations.size).toBe(0) + expect(uaIntents.get('queued-replacement')).toBe(true) + }) + + it('normal same-owner toggles preserve last-requested order and remove the promise tail', async () => { + const handle = register('serialized', 116) + const gate = pause(handle, 'Emulation.setTouchEmulationEnabled') + const first = browserManager.setViewportOverride('serialized', mobile) + await gate.entered + const second = browserManager.setViewportOverride('serialized', desktop) + const third = browserManager.setViewportOverride('serialized', null) + gate.resolve() + expect(await Promise.all([first, second, third])).toEqual([true, true, true]) + expect(handle.debuggerSendCommand.mock.calls.map(([method]) => method)).toEqual([ + 'Emulation.setDeviceMetricsOverride', + 'Emulation.setTouchEmulationEnabled', + 'Emulation.setUserAgentOverride', + 'Emulation.setDeviceMetricsOverride', + 'Emulation.setTouchEmulationEnabled', + 'Emulation.setUserAgentOverride', + 'Emulation.clearDeviceMetricsOverride', + 'Emulation.setTouchEmulationEnabled', + 'Emulation.setUserAgentOverride' + ]) + expect(pendingOperations.size).toBe(0) + expect(uaIntents.size).toBe(0) + }) + + it.each([false, true])( + 'keeps process-wide native UA behavior with mobile=%s', + async (mobileMode) => { + mocks.processUserAgentMode = 'native' + mocks.processUserAgent = GUEST_ELECTRON_UA + const handle = register('native', 117) + await expect( + browserManager.setViewportOverride('native', mobileMode ? mobile : desktop) + ).resolves.toBe(true) + expect(handle.debuggerSendCommand).toHaveBeenCalledWith( + 'Emulation.setUserAgentOverride', + mobileMode + ? expect.objectContaining({ userAgent: expect.stringContaining('iPhone') }) + : { userAgent: GUEST_ELECTRON_UA } + ) + expect(uaIntents.get('native')).toBe(mobileMode) + } + ) + + it('late rejected clears cannot repopulate all registries after unregisterAll', async () => { + const operations: { gate: ReturnType; pending: Promise }[] = [] + for (let index = 0; index < 16; index++) { + const tab = `all-closed-${index}` + const handle = register(tab, 200 + index) + await browserManager.setViewportOverride(tab, mobile) + const gate = pause(handle, 'Emulation.setUserAgentOverride') + const pending = browserManager.setViewportOverride(tab, null) + await gate.entered + operations.push({ gate, pending }) + } + browserManager.unregisterAll() + for (const { gate } of operations) { + gate.reject(new Error('Target closed')) + } + expect(await Promise.all(operations.map(({ pending }) => pending))).toEqual( + Array(16).fill(false) + ) + expect(uaIntents.size).toBe(0) + expect(registeredGuests.size).toBe(0) + expect(pendingOperations.size).toBe(0) + expect(presetIntents.size).toBe(0) + }) +}) diff --git a/src/main/browser/browser-manager-viewport.ts b/src/main/browser/browser-manager-viewport.ts index b5599ab4760..a4e263dcced 100644 --- a/src/main/browser/browser-manager-viewport.ts +++ b/src/main/browser/browser-manager-viewport.ts @@ -164,6 +164,9 @@ export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifec enabled: override.mobile, maxTouchPoints: override.mobile ? 5 : 0 }) + if (this.webContentsIdByTabId.get(browserTabId) !== webContentsId) { + return false + } // Navigation must see the preset while the final CDP write is in flight. this.viewportUaOverrideMobileByTabId.set(browserTabId, override.mobile) await this.sendViewportUserAgentOverride(guest, override.mobile) @@ -179,6 +182,9 @@ export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifec enabled: false, maxTouchPoints: 0 }) + if (this.webContentsIdByTabId.get(browserTabId) !== webContentsId) { + return false + } const trackedMobile = this.viewportUaOverrideMobileByTabId.get(browserTabId) // A navigation after this point must not re-install the override behind the clear. this.viewportUaOverrideMobileByTabId.delete(browserTabId) @@ -204,7 +210,10 @@ export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifec await dbg.sendCommand('Emulation.setUserAgentOverride', { userAgent: '' }) } } catch (error) { - if (trackedMobile !== undefined) { + if ( + trackedMobile !== undefined && + this.webContentsIdByTabId.get(browserTabId) === webContentsId + ) { this.viewportUaOverrideMobileByTabId.set(browserTabId, trackedMobile) } throw error From 98998b18ad2fe89f1a07dfe76788d73c96ec93f5 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:31 -0700 Subject: [PATCH 20/59] fix: release retired shared daemon owner metadata (#21162) Co-authored-by: m4air --- .../README.md | 67 + .../before.config.mjs | 23 + .../electron-results.json | 1582 +++++++++++++++++ .../fix.patch | 11 + .../node-results.json | 1581 ++++++++++++++++ .../publication-electron-results.json | 1574 ++++++++++++++++ .../publication-node-results.json | 1573 ++++++++++++++++ .../reproduce.cjs | 61 + .../scenario.cjs | 182 ++ .../source-versions.json | 1407 +++++++++++++++ .../sources.cjs | 100 ++ .../validation.json | 88 + .../daemon/daemon-session-owner-resolution.ts | 6 + ...shared-owner-incarnation-retention.test.ts | 157 ++ 14 files changed, 8412 insertions(+) create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/README.md create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/before.config.mjs create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/electron-results.json create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/fix.patch create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/node-results.json create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/publication-electron-results.json create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/publication-node-results.json create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/reproduce.cjs create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/scenario.cjs create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/source-versions.json create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/sources.cjs create mode 100644 docs/audits/daemon-shared-owner-incarnation-retention/validation.json create mode 100644 src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/README.md b/docs/audits/daemon-shared-owner-incarnation-retention/README.md new file mode 100644 index 00000000000..9ed1bf5008a --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/README.md @@ -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 daemon’s 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 provider’s 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 daemon’s 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 installation’s 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. diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/before.config.mjs b/docs/audits/daemon-shared-owner-incarnation-retention/before.config.mjs new file mode 100644 index 00000000000..1b24a9637cb --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/before.config.mjs @@ -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 + } + } + ] + }) +) diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/electron-results.json b/docs/audits/daemon-shared-owner-incarnation-retention/electron-results.json new file mode 100644 index 00000000000..5d2ee21f023 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/electron-results.json @@ -0,0 +1,1582 @@ +{ + "scope": "Actual degraded provider/recovery/resolvers, adapter inventory, identity publication and direct attach; finite inert authenticated transport replies, no native PTY or network", + "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" + }, + "sourceLabel": "working-tree", + "crlfReads": 2, + "artifactHashes": { + "sources.cjs": "e7d5bf87dcccdb6dac4ef867da67987a47a8209c88fb8f434bc9f169e8e231cf", + "scenario.cjs": "3a23e25aceb0f1591b3ae9934b1844ea51c8899614a437f9dfa16e26d4812bbf", + "reproduce.cjs": "6f66eb23c9fe171c920fe2fa8fd93127f927034b02e25260ef9d9268dee5d4f6", + "before.config.mjs": "4fcc9a02c35550a0e06e6791aa262fc72198d2aa2a6cf91bef5c59a6c7290f1b", + "source-versions.json": "d858b677fd572d1e7751ef23150985f77cbb6509b1323b721e140b23d98c1cae", + "fix.patch": "7e0554dda37dec5df920c4da9c54472abae949c657ebe938fd4692d0fc54660d" + }, + "phases": { + "before": { + "cycles": 32, + "rows": [ + { + "cycle": 0, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 2 + }, + { + "cycle": 1, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 3 + }, + { + "cycle": 2, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 4 + }, + { + "cycle": 3, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 5 + }, + { + "cycle": 4, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 6 + }, + { + "cycle": 5, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 7 + }, + { + "cycle": 6, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 8 + }, + { + "cycle": 7, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 9 + }, + { + "cycle": 8, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 10 + }, + { + "cycle": 9, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 11 + }, + { + "cycle": 10, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 12 + }, + { + "cycle": 11, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 13 + }, + { + "cycle": 12, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 14 + }, + { + "cycle": 13, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 15 + }, + { + "cycle": 14, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 16 + }, + { + "cycle": 15, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 17 + }, + { + "cycle": 16, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 18 + }, + { + "cycle": 17, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 19 + }, + { + "cycle": 18, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 20 + }, + { + "cycle": 19, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 21 + }, + { + "cycle": 20, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 22 + }, + { + "cycle": 21, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 23 + }, + { + "cycle": 22, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 24 + }, + { + "cycle": 23, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 25 + }, + { + "cycle": 24, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 26 + }, + { + "cycle": 25, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 27 + }, + { + "cycle": 26, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 28 + }, + { + "cycle": 27, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 29 + }, + { + "cycle": 28, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 30 + }, + { + "cycle": 29, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 31 + }, + { + "cycle": 30, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 32 + }, + { + "cycle": 31, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 33 + } + ], + "afterLegacyExit": { + "sharedRoutes": 0, + "attachEntries": 0, + "livenessEntries": 32 + }, + "sameIdSuccessorPreserved": true, + "matchingDirectAttachWithoutInventory": true, + "authoritativeIncarnationMismatchRefused": true, + "unchangedIdentityPreserved": true, + "otherLiveProviderPreserved": true, + "ordinaryExitRetiresBoth": true, + "provenance": { + "evaluatedSources": { + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-session-owner-resolution.ts": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "provenanceSources": { + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "bundleSha256": "4eff046149945b64f07bc36d94568ac6166d30f03f5a018396cbf710931407e9" + } + }, + "fixed": { + "cycles": 32, + "rows": [ + { + "cycle": 0, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 1, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 2, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 3, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 4, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 5, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 6, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 7, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 8, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 9, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 10, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 11, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 12, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 13, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 14, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 15, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 16, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 17, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 18, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 19, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 20, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 21, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 22, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 23, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 24, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 25, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 26, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 27, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 28, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 29, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 30, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 31, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + } + ], + "afterLegacyExit": { + "sharedRoutes": 0, + "attachEntries": 0, + "livenessEntries": 0 + }, + "sameIdSuccessorPreserved": true, + "matchingDirectAttachWithoutInventory": true, + "authoritativeIncarnationMismatchRefused": true, + "unchangedIdentityPreserved": true, + "otherLiveProviderPreserved": true, + "ordinaryExitRetiresBoth": true, + "provenance": { + "evaluatedSources": { + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "provenanceSources": { + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "bundleSha256": "310cba6f434f170b7554db5b9777041865ffc294773ad0c9966f0a0a60ad4885" + } + } + } +} diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/fix.patch b/docs/audits/daemon-shared-owner-incarnation-retention/fix.patch new file mode 100644 index 00000000000..37a7896c132 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/fix.patch @@ -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 { ++ // 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) ++ } ++ } diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/node-results.json b/docs/audits/daemon-shared-owner-incarnation-retention/node-results.json new file mode 100644 index 00000000000..3027ef27dbd --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/node-results.json @@ -0,0 +1,1581 @@ +{ + "scope": "Actual degraded provider/recovery/resolvers, adapter inventory, identity publication and direct attach; finite inert authenticated transport replies, no native PTY or network", + "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" + }, + "sourceLabel": "working-tree", + "crlfReads": 2, + "artifactHashes": { + "sources.cjs": "e7d5bf87dcccdb6dac4ef867da67987a47a8209c88fb8f434bc9f169e8e231cf", + "scenario.cjs": "3a23e25aceb0f1591b3ae9934b1844ea51c8899614a437f9dfa16e26d4812bbf", + "reproduce.cjs": "6f66eb23c9fe171c920fe2fa8fd93127f927034b02e25260ef9d9268dee5d4f6", + "before.config.mjs": "4fcc9a02c35550a0e06e6791aa262fc72198d2aa2a6cf91bef5c59a6c7290f1b", + "source-versions.json": "d858b677fd572d1e7751ef23150985f77cbb6509b1323b721e140b23d98c1cae", + "fix.patch": "7e0554dda37dec5df920c4da9c54472abae949c657ebe938fd4692d0fc54660d" + }, + "phases": { + "before": { + "cycles": 32, + "rows": [ + { + "cycle": 0, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 2 + }, + { + "cycle": 1, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 3 + }, + { + "cycle": 2, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 4 + }, + { + "cycle": 3, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 5 + }, + { + "cycle": 4, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 6 + }, + { + "cycle": 5, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 7 + }, + { + "cycle": 6, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 8 + }, + { + "cycle": 7, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 9 + }, + { + "cycle": 8, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 10 + }, + { + "cycle": 9, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 11 + }, + { + "cycle": 10, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 12 + }, + { + "cycle": 11, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 13 + }, + { + "cycle": 12, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 14 + }, + { + "cycle": 13, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 15 + }, + { + "cycle": 14, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 16 + }, + { + "cycle": 15, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 17 + }, + { + "cycle": 16, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 18 + }, + { + "cycle": 17, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 19 + }, + { + "cycle": 18, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 20 + }, + { + "cycle": 19, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 21 + }, + { + "cycle": 20, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 22 + }, + { + "cycle": 21, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 23 + }, + { + "cycle": 22, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 24 + }, + { + "cycle": 23, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 25 + }, + { + "cycle": 24, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 26 + }, + { + "cycle": 25, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 27 + }, + { + "cycle": 26, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 28 + }, + { + "cycle": 27, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 29 + }, + { + "cycle": 28, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 30 + }, + { + "cycle": 29, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 31 + }, + { + "cycle": 30, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 32 + }, + { + "cycle": 31, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 33 + } + ], + "afterLegacyExit": { + "sharedRoutes": 0, + "attachEntries": 0, + "livenessEntries": 32 + }, + "sameIdSuccessorPreserved": true, + "matchingDirectAttachWithoutInventory": true, + "authoritativeIncarnationMismatchRefused": true, + "unchangedIdentityPreserved": true, + "otherLiveProviderPreserved": true, + "ordinaryExitRetiresBoth": true, + "provenance": { + "evaluatedSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-session-owner-resolution.ts": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee" + }, + "provenanceSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee" + }, + "bundleSha256": "4eff046149945b64f07bc36d94568ac6166d30f03f5a018396cbf710931407e9" + } + }, + "fixed": { + "cycles": 32, + "rows": [ + { + "cycle": 0, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 1, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 2, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 3, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 4, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 5, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 6, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 7, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 8, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 9, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 10, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 11, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 12, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 13, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 14, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 15, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 16, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 17, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 18, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 19, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 20, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 21, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 22, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 23, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 24, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 25, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 26, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 27, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 28, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 29, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 30, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 31, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + } + ], + "afterLegacyExit": { + "sharedRoutes": 0, + "attachEntries": 0, + "livenessEntries": 0 + }, + "sameIdSuccessorPreserved": true, + "matchingDirectAttachWithoutInventory": true, + "authoritativeIncarnationMismatchRefused": true, + "unchangedIdentityPreserved": true, + "otherLiveProviderPreserved": true, + "ordinaryExitRetiresBoth": true, + "provenance": { + "evaluatedSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "provenanceSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "bundleSha256": "310cba6f434f170b7554db5b9777041865ffc294773ad0c9966f0a0a60ad4885" + } + } + } +} diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/publication-electron-results.json b/docs/audits/daemon-shared-owner-incarnation-retention/publication-electron-results.json new file mode 100644 index 00000000000..9f2e6126e62 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/publication-electron-results.json @@ -0,0 +1,1574 @@ +{ + "scope": "Actual degraded provider/recovery/resolvers, adapter inventory, identity publication and direct attach; finite inert authenticated transport replies, no native PTY or network", + "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" + }, + "sourceLabel": "publication-291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "crlfReads": 2, + "artifactHashes": { + "sources.cjs": "e7d5bf87dcccdb6dac4ef867da67987a47a8209c88fb8f434bc9f169e8e231cf", + "scenario.cjs": "3a23e25aceb0f1591b3ae9934b1844ea51c8899614a437f9dfa16e26d4812bbf", + "reproduce.cjs": "6f66eb23c9fe171c920fe2fa8fd93127f927034b02e25260ef9d9268dee5d4f6", + "before.config.mjs": "4fcc9a02c35550a0e06e6791aa262fc72198d2aa2a6cf91bef5c59a6c7290f1b", + "source-versions.json": "d858b677fd572d1e7751ef23150985f77cbb6509b1323b721e140b23d98c1cae", + "fix.patch": "7e0554dda37dec5df920c4da9c54472abae949c657ebe938fd4692d0fc54660d" + }, + "phases": { + "before": { + "cycles": 32, + "rows": [ + { + "cycle": 0, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 2 + }, + { + "cycle": 1, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 3 + }, + { + "cycle": 2, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 4 + }, + { + "cycle": 3, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 5 + }, + { + "cycle": 4, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 6 + }, + { + "cycle": 5, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 7 + }, + { + "cycle": 6, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 8 + }, + { + "cycle": 7, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 9 + }, + { + "cycle": 8, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 10 + }, + { + "cycle": 9, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 11 + }, + { + "cycle": 10, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 12 + }, + { + "cycle": 11, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 13 + }, + { + "cycle": 12, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 14 + }, + { + "cycle": 13, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 15 + }, + { + "cycle": 14, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 16 + }, + { + "cycle": 15, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 17 + }, + { + "cycle": 16, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 18 + }, + { + "cycle": 17, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 19 + }, + { + "cycle": 18, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 20 + }, + { + "cycle": 19, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 21 + }, + { + "cycle": 20, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 22 + }, + { + "cycle": 21, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 23 + }, + { + "cycle": 22, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 24 + }, + { + "cycle": 23, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 25 + }, + { + "cycle": 24, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 26 + }, + { + "cycle": 25, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 27 + }, + { + "cycle": 26, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 28 + }, + { + "cycle": 27, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 29 + }, + { + "cycle": 28, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 30 + }, + { + "cycle": 29, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 31 + }, + { + "cycle": 30, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 32 + }, + { + "cycle": 31, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 33 + } + ], + "afterLegacyExit": { + "sharedRoutes": 0, + "attachEntries": 0, + "livenessEntries": 32 + }, + "sameIdSuccessorPreserved": true, + "matchingDirectAttachWithoutInventory": true, + "authoritativeIncarnationMismatchRefused": true, + "unchangedIdentityPreserved": true, + "otherLiveProviderPreserved": true, + "ordinaryExitRetiresBoth": true, + "provenance": { + "evaluatedSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-session-owner-resolution.ts": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "provenanceSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "bundleSha256": "c55b12ceb272a2c3eab948ba3eb72c2033a7f1a2f66e67c4a372b456611e6d8a" + } + }, + "fixed": { + "cycles": 32, + "rows": [ + { + "cycle": 0, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 1, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 2, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 3, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 4, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 5, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 6, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 7, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 8, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 9, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 10, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 11, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 12, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 13, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 14, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 15, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 16, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 17, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 18, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 19, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 20, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 21, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 22, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 23, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 24, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 25, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 26, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 27, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 28, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 29, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 30, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 31, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + } + ], + "afterLegacyExit": { + "sharedRoutes": 0, + "attachEntries": 0, + "livenessEntries": 0 + }, + "sameIdSuccessorPreserved": true, + "matchingDirectAttachWithoutInventory": true, + "authoritativeIncarnationMismatchRefused": true, + "unchangedIdentityPreserved": true, + "otherLiveProviderPreserved": true, + "ordinaryExitRetiresBoth": true, + "provenance": { + "evaluatedSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee" + }, + "provenanceSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee" + }, + "bundleSha256": "b4999fa3a0b12667092086872d910c358b94730e871b759411c77315525383c2" + } + } + } +} diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/publication-node-results.json b/docs/audits/daemon-shared-owner-incarnation-retention/publication-node-results.json new file mode 100644 index 00000000000..b4fbbf6ed06 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/publication-node-results.json @@ -0,0 +1,1573 @@ +{ + "scope": "Actual degraded provider/recovery/resolvers, adapter inventory, identity publication and direct attach; finite inert authenticated transport replies, no native PTY or network", + "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" + }, + "sourceLabel": "publication-291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "crlfReads": 2, + "artifactHashes": { + "sources.cjs": "e7d5bf87dcccdb6dac4ef867da67987a47a8209c88fb8f434bc9f169e8e231cf", + "scenario.cjs": "3a23e25aceb0f1591b3ae9934b1844ea51c8899614a437f9dfa16e26d4812bbf", + "reproduce.cjs": "6f66eb23c9fe171c920fe2fa8fd93127f927034b02e25260ef9d9268dee5d4f6", + "before.config.mjs": "4fcc9a02c35550a0e06e6791aa262fc72198d2aa2a6cf91bef5c59a6c7290f1b", + "source-versions.json": "d858b677fd572d1e7751ef23150985f77cbb6509b1323b721e140b23d98c1cae", + "fix.patch": "7e0554dda37dec5df920c4da9c54472abae949c657ebe938fd4692d0fc54660d" + }, + "phases": { + "before": { + "cycles": 32, + "rows": [ + { + "cycle": 0, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 2 + }, + { + "cycle": 1, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 3 + }, + { + "cycle": 2, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 4 + }, + { + "cycle": 3, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 5 + }, + { + "cycle": 4, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 6 + }, + { + "cycle": 5, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 7 + }, + { + "cycle": 6, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 8 + }, + { + "cycle": 7, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 9 + }, + { + "cycle": 8, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 10 + }, + { + "cycle": 9, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 11 + }, + { + "cycle": 10, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 12 + }, + { + "cycle": 11, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 13 + }, + { + "cycle": 12, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 14 + }, + { + "cycle": 13, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 15 + }, + { + "cycle": 14, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 16 + }, + { + "cycle": 15, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 17 + }, + { + "cycle": 16, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 18 + }, + { + "cycle": 17, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 19 + }, + { + "cycle": 18, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 20 + }, + { + "cycle": 19, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 21 + }, + { + "cycle": 20, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 22 + }, + { + "cycle": 21, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 23 + }, + { + "cycle": 22, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 24 + }, + { + "cycle": 23, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 25 + }, + { + "cycle": 24, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 26 + }, + { + "cycle": 25, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 27 + }, + { + "cycle": 26, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 28 + }, + { + "cycle": 27, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 29 + }, + { + "cycle": 28, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 30 + }, + { + "cycle": 29, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 31 + }, + { + "cycle": 30, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 32 + }, + { + "cycle": 31, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 33 + } + ], + "afterLegacyExit": { + "sharedRoutes": 0, + "attachEntries": 0, + "livenessEntries": 32 + }, + "sameIdSuccessorPreserved": true, + "matchingDirectAttachWithoutInventory": true, + "authoritativeIncarnationMismatchRefused": true, + "unchangedIdentityPreserved": true, + "otherLiveProviderPreserved": true, + "ordinaryExitRetiresBoth": true, + "provenance": { + "evaluatedSources": { + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-session-owner-resolution.ts": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "provenanceSources": { + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + }, + "bundleSha256": "c55b12ceb272a2c3eab948ba3eb72c2033a7f1a2f66e67c4a372b456611e6d8a" + } + }, + "fixed": { + "cycles": 32, + "rows": [ + { + "cycle": 0, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 1, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 2, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 3, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 4, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 5, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 6, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 7, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 8, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 9, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 10, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 11, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 12, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 13, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 14, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 15, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 16, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 17, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 18, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 19, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 20, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 21, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 22, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 23, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 24, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 25, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 26, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 27, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 28, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 29, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 30, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + }, + { + "cycle": 31, + "sharedRoutes": 1, + "attachEntries": 1, + "livenessEntries": 1 + } + ], + "afterLegacyExit": { + "sharedRoutes": 0, + "attachEntries": 0, + "livenessEntries": 0 + }, + "sameIdSuccessorPreserved": true, + "matchingDirectAttachWithoutInventory": true, + "authoritativeIncarnationMismatchRefused": true, + "unchangedIdentityPreserved": true, + "otherLiveProviderPreserved": true, + "ordinaryExitRetiresBoth": true, + "provenance": { + "evaluatedSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee" + }, + "provenanceSources": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/daemon-session-owner-resolution.ts": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee" + }, + "bundleSha256": "b4999fa3a0b12667092086872d910c358b94730e871b759411c77315525383c2" + } + } + } +} diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/reproduce.cjs b/docs/audits/daemon-shared-owner-incarnation-retention/reproduce.cjs new file mode 100644 index 00000000000..7113e511f75 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/reproduce.cjs @@ -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 + }) +} diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/scenario.cjs b/docs/audits/daemon-shared-owner-incarnation-retention/scenario.cjs new file mode 100644 index 00000000000..816e5795e15 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/scenario.cjs @@ -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 } diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/source-versions.json b/docs/audits/daemon-shared-owner-incarnation-retention/source-versions.json new file mode 100644 index 00000000000..b8022423ee8 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/source-versions.json @@ -0,0 +1,1407 @@ +{ + "sourcePath": "src/main/daemon/daemon-session-owner-resolution.ts", + "baselineSha256": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "fixedSha256": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb", + "dependencies": { + "src/main/daemon/degraded-daemon-pty-provider.ts": [ + "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd" + ], + "src/main/daemon/daemon-pty-adapter.ts": [ + "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc" + ], + "src/main/daemon/types.ts": [ + "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d" + ], + "src/main/daemon/daemon-pty-daemon-recovery.ts": [ + "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce" + ], + "src/main/daemon/degraded-daemon-owner-recovery.ts": [ + "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6" + ], + "src/main/providers/pty-process-inspection.ts": [ + "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20" + ], + "src/main/daemon/degraded-daemon-session-routing.ts": [ + "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b" + ], + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": [ + "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb" + ], + "src/main/daemon/combine-unsubscribes.ts": [ + "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4" + ], + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": [ + "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff" + ], + "src/main/daemon/daemon-errors.ts": [ + "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac" + ], + "src/main/daemon/daemon-protocol-version.ts": [ + "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd" + ], + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": [ + "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0" + ], + "src/main/daemon/daemon-health.ts": [ + "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114" + ], + "src/main/daemon/daemon-tcc-attribution.ts": [ + "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923" + ], + "src/main/daemon/daemon-bundle-staleness.ts": [ + "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1" + ], + "src/main/daemon/daemon-endpoint-errors.ts": [ + "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7" + ], + "src/shared/terminal-process-inspection.ts": [ + "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b" + ], + "src/main/daemon/daemon-endpoint-ownership.ts": [ + "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc" + ], + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": [ + "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1" + ], + "src/main/daemon/daemon-durable-history-snapshot.ts": [ + "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072" + ], + "src/main/daemon/daemon-pid-identity.ts": [ + "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190" + ], + "src/main/daemon/daemon-spawner.ts": [ + "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017" + ], + "src/main/daemon/daemon-pid-file-parse.ts": [ + "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51" + ], + "src/main/daemon/ndjson.ts": [ + "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91" + ], + "src/shared/main-process-ndjson-framer.ts": [ + "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490" + ], + "src/main/daemon/daemon-process-start-time.ts": [ + "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3" + ], + "src/main/daemon/daemon-process-identity-query.ts": [ + "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b" + ], + "src/main/daemon/daemon-respawn-throttle.ts": [ + "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645" + ], + "src/main/daemon/daemon-endpoint-probe.ts": [ + "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52" + ], + "src/main/daemon/daemon-pty-connection-lifecycle.ts": [ + "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef" + ], + "src/main/daemon/daemon-request-deadline.ts": [ + "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10" + ], + "src/main/daemon/terminal-history-dimensions.ts": [ + "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b" + ], + "src/main/daemon/headless-emulator.ts": [ + "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7" + ], + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": [ + "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c" + ], + "src/main/daemon/cold-restore-replay-writer.ts": [ + "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5" + ], + "src/main/daemon/daemon-restore-scrollback-depth.ts": [ + "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d" + ], + "src/shared/process-output-field-scanner.ts": [ + "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5" + ], + "src/main/startup/startup-diagnostics.ts": [ + "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e" + ], + "src/main/daemon/daemon-pty-event-subscriptions.ts": [ + "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e" + ], + "src/main/daemon/daemon-listener-registry.ts": [ + "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449" + ], + "src/main/daemon/daemon-endpoint-incarnation.ts": [ + "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07" + ], + "src/main/daemon/daemon-audit-classifier.ts": [ + "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b" + ], + "src/shared/terminal-scrollback-policy.ts": [ + "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17" + ], + "src/main/daemon/headless-emulator-modes.ts": [ + "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807" + ], + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": [ + "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca" + ], + "src/main/daemon/terminal-mouse-mode-mirror.ts": [ + "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1" + ], + "src/shared/terminal-serialize-absolute-cursor.ts": [ + "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9" + ], + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": [ + "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d" + ], + "src/shared/terminal-partial-escape-tail.ts": [ + "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29" + ], + "src/main/daemon/terminal-frame-restore-sequences.ts": [ + "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7" + ], + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": [ + "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee" + ], + "src/main/daemon/terminal-view-attribute-responder.ts": [ + "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7" + ], + "src/main/daemon/startup-device-attributes-responder.ts": [ + "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b" + ], + "src/shared/terminal-cursor-line-context.ts": [ + "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1" + ], + "src/shared/terminal-osc-link-retirement.ts": [ + "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d" + ], + "src/shared/terminal-unicode-provider.ts": [ + "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f" + ], + "src/main/daemon/headless-osc-link-ranges.ts": [ + "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca" + ], + "src/main/daemon/xterm-env-polyfill.ts": [ + "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9" + ], + "src/main/daemon/daemon-pty-session-inventory.ts": [ + "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad" + ], + "src/main/daemon/daemon-incarnation-evidence.ts": [ + "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb" + ], + "src/shared/terminal-mode-reset-profiles.ts": [ + "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049" + ], + "src/shared/own-retained-string.ts": [ + "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2" + ], + "src/main/daemon/osc7-uri-extraction.ts": [ + "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7" + ], + "src/shared/agent-detection.ts": [ + "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651" + ], + "src/main/daemon/osc7-file-uri.ts": [ + "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a" + ], + "src/shared/terminal-escape-introducer.ts": [ + "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5" + ], + "src/shared/terminal-view-attributes.ts": [ + "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305" + ], + "src/main/providers/pty-process-list-admission.ts": [ + "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356" + ], + "src/shared/wsl-paths.ts": ["1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a"], + "src/shared/claimed-agent-pty-owner-snapshot.ts": [ + "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5" + ], + "src/shared/agent-session-host-authority.ts": [ + "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7" + ], + "src/main/daemon/daemon-pty-process-inspection.ts": [ + "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8" + ], + "src/main/daemon/pty-session-id.ts": [ + "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878" + ], + "src/shared/agent-title-core.ts": [ + "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d" + ], + "src/shared/opencode-terminal-title.ts": [ + "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec" + ], + "src/shared/agent-title-identity.ts": [ + "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453" + ], + "src/shared/agent-title-status.ts": [ + "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb" + ], + "src/shared/claimed-agent-pty-owner.ts": [ + "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1" + ], + "src/shared/agent-name-token-match.ts": [ + "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8" + ], + "src/shared/osc-title-extraction.ts": [ + "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a" + ], + "src/shared/shell-process-detection.ts": [ + "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f" + ], + "src/shared/owned-utf16-suffix.ts": [ + "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + ], + "src/main/daemon/daemon-process-inspection.ts": [ + "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683" + ], + "src/main/agent-hooks/managed-hook-owner-identity.ts": [ + "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c" + ], + "src/main/daemon/daemon-incarnation-evidence-types.ts": [ + "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8" + ], + "src/shared/foreground-process-evidence.ts": [ + "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c" + ], + "src/shared/pty-incarnation.ts": [ + "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5" + ], + "src/shared/terminal-tab-id.ts": [ + "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82" + ], + "src/shared/stable-pane-id.ts": [ + "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884" + ], + "src/shared/protocol-version.ts": [ + "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954" + ], + "src/shared/agent-session-resume.ts": [ + "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43" + ], + "src/shared/terminal-title-classification-memo.ts": [ + "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda" + ], + "src/shared/pi-state-title-marker.ts": [ + "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8" + ], + "src/shared/pi-compatible-synthetic-title.ts": [ + "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f" + ], + "src/shared/pty-session-id-format.ts": [ + "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b" + ], + "src/main/daemon/daemon-pty-buffer-snapshots.ts": [ + "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9" + ], + "src/shared/terminal-title-wrapper-segments.ts": [ + "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78" + ], + "src/shared/agent-title-decoration.ts": [ + "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41" + ], + "src/shared/terminal-title-agent-type.ts": [ + "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864" + ], + "src/shared/terminal-surface-id.ts": [ + "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da" + ], + "src/shared/skill-install-capability.ts": [ + "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19" + ], + "src/shared/remote-server-update.ts": [ + "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12" + ], + "src/shared/workspace-scope.ts": [ + "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581" + ], + "src/main/daemon/daemon-session-scrollback-window.ts": [ + "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68" + ], + "src/shared/terminal-kitty-keyboard-flags.ts": [ + "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225" + ], + "src/main/daemon/daemon-pty-session-control.ts": [ + "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8" + ], + "src/shared/pane-agent-identity-adapter.ts": [ + "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9" + ], + "src/main/daemon/daemon-pty-applied-size.ts": [ + "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0" + ], + "src/main/providers/pty-default-cwd.ts": [ + "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f" + ], + "src/main/daemon/wsl-cold-restore-cwd.ts": [ + "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200" + ], + "src/main/daemon/daemon-pty-session-input.ts": [ + "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037" + ], + "src/main/daemon/daemon-pty-lifecycle-errors.ts": [ + "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1" + ], + "src/shared/agent-title-evidence.ts": [ + "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea" + ], + "src/shared/pane-agent-evidence-sources.ts": [ + "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f" + ], + "src/main/providers/pty-path-safety.ts": [ + "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0" + ], + "src/main/daemon/daemon-pty-size.ts": [ + "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741" + ], + "src/shared/pty-write-settlement.ts": [ + "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6" + ], + "src/main/daemon/daemon-pty-session-spawn.ts": [ + "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114" + ], + "src/main/providers/pty-write-unavailable-error.ts": [ + "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40" + ], + "src/shared/tui-agent-display-names.ts": [ + "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296" + ], + "src/shared/synthetic-agent-title.ts": [ + "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + ], + "src/shared/agent-process-recognition.ts": [ + "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4" + ], + "src/main/daemon/session-shell-ready-barrier.ts": [ + "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109" + ], + "src/main/daemon/daemon-pty-spawn-result.ts": [ + "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e" + ], + "src/shared/codex-startup-delivery.ts": [ + "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad" + ], + "src/main/daemon/daemon-adoption-telemetry-event.ts": [ + "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d" + ], + "src/main/wsl-env.ts": ["66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52"], + "src/main/daemon/terminal-history-seed-segments.ts": [ + "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5" + ], + "src/main/terminal-history.ts": [ + "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013" + ], + "src/main/daemon/shell-ready.ts": [ + "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669" + ], + "src/main/providers/local-pty-utils.ts": [ + "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25" + ], + "src/shared/shell-ready-marker-timing.ts": [ + "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2" + ], + "src/main/daemon/wsl-session-context.ts": [ + "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a" + ], + "src/main/shell-prompt-readiness-probe.ts": [ + "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73" + ], + "src/main/daemon/post-ready-flush-gate.ts": [ + "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac" + ], + "src/main/shell-startup-output-scanner.ts": [ + "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7" + ], + "src/shared/command-token-scanner.ts": [ + "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0" + ], + "src/shared/tui-agent-config.ts": [ + "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad" + ], + "src/shared/agent-node-entrypoint-identities.ts": [ + "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4" + ], + "src/shared/agent-headless-command.ts": [ + "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87" + ], + "src/main/daemon/daemon-history-recovery-freeze.ts": [ + "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0" + ], + "src/shared/wsl-env.ts": ["9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88"], + "src/main/daemon/daemon-attach-only-retirement.ts": [ + "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca" + ], + "src/main/daemon/daemon-pty-spawn-request.ts": [ + "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e" + ], + "src/main/daemon/daemon-pty-provider-sequence.ts": [ + "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb" + ], + "src/main/telemetry/client.ts": [ + "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0" + ], + "src/shared/app-environment.ts": [ + "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140" + ], + "src/shared/daemon-adoption-telemetry.ts": [ + "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5" + ], + "src/shared/daemon-lifecycle-telemetry.ts": [ + "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc" + ], + "src/main/line-editor-ready-output-scanner.ts": [ + "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293" + ], + "src/shared/pty-slave-line-discipline-echo.ts": [ + "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4" + ], + "src/shared/shell-process-readiness.ts": [ + "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd" + ], + "src/main/telemetry/validator.ts": [ + "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99" + ], + "src/main/telemetry/cohort-classifier.ts": [ + "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f" + ], + "src/main/telemetry/consent.ts": [ + "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39" + ], + "src/main/telemetry/burst-cap.ts": [ + "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c" + ], + "src/main/providers/working-directory-validation.ts": [ + "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043" + ], + "src/shared/node-pty-spawn-helper.ts": [ + "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d" + ], + "src/main/providers/macos-tcc-login-shell.ts": [ + "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba" + ], + "src/main/daemon/terminal-history-seed-chunks.ts": [ + "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f" + ], + "src/main/daemon/daemon-pty-runtime-state.ts": [ + "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19" + ], + "src/shared/print-mode-headless-command.ts": [ + "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a" + ], + "src/shared/prime-agent-headless-command.ts": [ + "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3" + ], + "src/shared/ante-headless-command.ts": [ + "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620" + ], + "src/main/shell-startup-identity-scanner.ts": [ + "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6" + ], + "src/main/shell-ready-marker-scanner.ts": [ + "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d" + ], + "src/shared/orca-cli-command-name.ts": [ + "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0" + ], + "src/main/wsl.ts": ["d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a"], + "src/shared/worktree/id.ts": [ + "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30" + ], + "src/shared/process-table-snapshot.ts": [ + "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9" + ], + "src/shared/local-windows-terminal-runtime.ts": [ + "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c" + ], + "src/main/terminal-history-id.ts": [ + "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0" + ], + "src/main/fish-history-session.ts": [ + "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb" + ], + "src/main/daemon/daemon-shell-ready-marker.ts": [ + "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21" + ], + "src/main/shell-wrapper-content-address.ts": [ + "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028" + ], + "src/main/shell-wrapper-file-writer.ts": [ + "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232" + ], + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": [ + "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b" + ], + "src/main/worktree-history-file-path.ts": [ + "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa" + ], + "src/shared/telemetry-events.ts": [ + "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6" + ], + "src/main/pty/codex-shell-launch-preflight.ts": [ + "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca" + ], + "src/main/terminal-history-paths.ts": [ + "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75" + ], + "src/main/zsh-wrapper-dir-ownership.ts": [ + "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e" + ], + "src/main/shell-templates.ts": [ + "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a" + ], + "src/main/powershell-osc133-bootstrap.ts": [ + "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446" + ], + "src/main/shell-startup-features.ts": [ + "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7" + ], + "src/shared/priority-semaphore.ts": [ + "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5" + ], + "src/main/providers/macos-login-session-pty-probe.ts": [ + "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63" + ], + "src/shared/child-process/run-process.ts": [ + "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad" + ], + "src/shared/cross-platform-path.ts": [ + "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3" + ], + "src/main/daemon/history-reader.ts": [ + "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d" + ], + "src/main/daemon/client.ts": [ + "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14" + ], + "src/main/daemon/daemon-audit-eligibility-event.ts": [ + "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137" + ], + "src/main/daemon/daemon-checkpoint-session-queue.ts": [ + "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b" + ], + "src/main/daemon/cold-restore-payload-cache.ts": [ + "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77" + ], + "src/main/daemon/history-manager.ts": [ + "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c" + ], + "src/main/wsl-directory-probe-command.ts": [ + "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246" + ], + "src/main/wsl-distro-list-output.ts": [ + "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc" + ], + "src/main/wsl-availability.ts": [ + "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf" + ], + "src/main/wsl-interop-spawn-directory.ts": [ + "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf" + ], + "src/main/wsl-running-distro-cache.ts": [ + "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a" + ], + "src/main/wsl-distro-retry.ts": [ + "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be" + ], + "src/main/cli/bundled-cli-launcher-path.ts": [ + "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4" + ], + "src/shared/powershell-command-encoding.ts": [ + "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9" + ], + "src/main/pty/omp-shell-wrapper.ts": [ + "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf" + ], + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": [ + "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b" + ], + "src/main/zsh-startup-wrapper-builder.ts": [ + "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26" + ], + "src/main/bash-prompt-command-composition.ts": [ + "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d" + ], + "src/main/pty/posix-shell-startup-command.ts": [ + "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c" + ], + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": [ + "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be" + ], + "src/shared/telemetry-app-event-schemas.ts": [ + "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831" + ], + "src/shared/telemetry-daemon-event-schemas.ts": [ + "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac" + ], + "src/shared/telemetry-property-schemas.ts": [ + "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd" + ], + "src/shared/telemetry-event-classification.ts": [ + "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e" + ], + "src/shared/telemetry-event-registry.ts": [ + "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9" + ], + "src/shared/wsl-login-shell-command.ts": [ + "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075" + ], + "src/shared/child-process/process-spec.ts": [ + "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289" + ], + "src/shared/child-process/bounded-output-sink.ts": [ + "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865" + ], + "src/shared/child-process/child-termination-reporter.ts": [ + "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0" + ], + "src/shared/child-process/process-tree-termination.ts": [ + "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120" + ], + "src/shared/child-process/spawn-resolution.ts": [ + "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc" + ], + "src/main/daemon/history-paths.ts": [ + "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63" + ], + "src/main/daemon/terminal-history-log.ts": [ + "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb" + ], + "src/main/daemon/terminal-history-file-limits.ts": [ + "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a" + ], + "src/main/daemon/terminal-history-recovery-quarantine.ts": [ + "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85" + ], + "src/main/daemon/terminal-history-file-reader.ts": [ + "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae" + ], + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": [ + "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441" + ], + "src/main/daemon/terminal-history-restorable-retention.ts": [ + "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d" + ], + "src/main/daemon/daemon-private-file-modes.ts": [ + "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615" + ], + "src/main/daemon/terminal-history-session-tombstone.ts": [ + "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6" + ], + "src/main/daemon/terminal-history-metadata.ts": [ + "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084" + ], + "src/main/daemon/terminal-history-session-files.ts": [ + "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb" + ], + "src/main/daemon/terminal-history-cold-restore-info.ts": [ + "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680" + ], + "src/main/daemon/terminal-history-checkpoint-reader.ts": [ + "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1" + ], + "src/main/daemon/terminal-history-recovery-freezes.ts": [ + "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a" + ], + "src/main/daemon/terminal-history-session-writer.ts": [ + "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856" + ], + "src/main/daemon/terminal-history-mutation-tracker.ts": [ + "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e" + ], + "src/shared/star-nag-telemetry.ts": [ + "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b" + ], + "src/shared/gh-star-source.ts": [ + "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe" + ], + "src/shared/feature-interactions.ts": [ + "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a" + ], + "src/shared/daemon-audit-eligibility.ts": [ + "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446" + ], + "src/shared/agent-hook-types.ts": [ + "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b" + ], + "src/shared/telemetry-feature-education-event-schemas.ts": [ + "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe" + ], + "src/shared/telemetry-native-feature-event-schemas.ts": [ + "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d" + ], + "src/main/daemon/daemon-client-listener-registry.ts": [ + "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b" + ], + "src/main/daemon/daemon-client-pending-requests.ts": [ + "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b" + ], + "src/main/daemon/daemon-client-hello-handshake.ts": [ + "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73" + ], + "src/main/daemon/daemon-client-ndjson-readers.ts": [ + "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee" + ], + "src/shared/telemetry-onboarding-event-schemas.ts": [ + "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756" + ], + "src/main/daemon/daemon-client-notify-settlement.ts": [ + "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25" + ], + "src/main/daemon/daemon-client-socket-connect.ts": [ + "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76" + ], + "src/shared/telemetry-repository-event-schemas.ts": [ + "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572" + ], + "src/main/daemon/daemon-client-rpc-request.ts": [ + "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b" + ], + "src/shared/child-process/windows-cmd-shim-resolution.ts": [ + "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3" + ], + "src/shared/child-process/windows-command-line.ts": [ + "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca" + ], + "src/shared/workspace-source.ts": [ + "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29" + ], + "src/shared/feature-wall-tour-depth.ts": [ + "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb" + ], + "src/shared/setup-script-import-providers.ts": [ + "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a" + ], + "src/shared/child-process/process-tree-kill-gate.ts": [ + "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab" + ], + "src/shared/node-bounded-file-reader.ts": [ + "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb" + ], + "src/main/daemon/terminal-checkpoint-serializer.ts": [ + "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375" + ], + "src/shared/terminal-osc-link-ranges.ts": [ + "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390" + ], + "src/shared/terminal-owner.ts": [ + "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309" + ], + "src/main/host-tree-removal.ts": [ + "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97" + ], + "src/shared/feature-interaction-categories.ts": [ + "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d" + ], + "src/main/daemon/node-pty-error-hints.ts": [ + "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a" + ], + "src/shared/feature-interaction-catalog.ts": [ + "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80" + ], + "src/shared/feature-interaction-usage-buckets.ts": [ + "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff" + ], + "src/shared/feature-education-telemetry.ts": [ + "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00" + ], + "src/shared/feature-wall-setup-steps.ts": [ + "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d" + ], + "src/shared/feature-wall-telemetry.ts": [ + "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693" + ], + "src/main/asar-transparent-fs.ts": [ + "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee" + ], + "src/shared/telemetry-onboarding-foundation-schemas.ts": [ + "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3" + ], + "src/shared/windows-transient-lock-removal.ts": [ + "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2" + ], + "src/shared/nested-repo-telemetry.ts": [ + "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7" + ], + "src/main/daemon/daemon-pty-listener-emission.ts": [ + "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f" + ] + }, + "workingRevision": "6ab6802df90553d2a26e9a4c7a519ae11d4b5e09", + "publicationRevision": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "reportedRevision": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "historicalCore": { + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "src/main/daemon/daemon-session-owner-resolution.ts": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef" + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "src/main/daemon/daemon-session-owner-resolution.ts": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-pty-provider.ts": "108dc9bf12eaaaa1f446c1a86e264611c5a28cd9dfc4f2e8bc59baca168a9d59", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef" + } + }, + "workingEvaluated": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "edb242c76bba5e823ebd84e87260bcce564ec928d18ca7d3f10f43c3bcc85c88", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "f12d5516b6b40859256fa56593ab58484482be39c59b1176773cda58b5ce78f2", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-session-owner-resolution.ts": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "85c68ffd0e20dad097fe32b8fcf39de7c4e89cee75e7c76a3b4699bad3e6913c", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/shared/terminal-osc-link-retirement.ts": "40afa50eb7f531a2ff8df54b2fb0d86761597b635a1d6009e22503efdbd0113d", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/main/daemon/daemon-pty-session-inventory.ts": "37a9ff93dd9a227f708597c3eafecf886350e7d3c8b5ad4468149513775a8f4c", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/protocol-version.ts": "397ce01879e98b4c8f6b0068ce8c0d180e34c65663db66f65016790bd31f4e6d", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/providers/working-directory-validation.ts": "0ef75b5d5242ad7e5423bde2cb7fe633a8f437fc545f22ca04612782fc2de212", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/daemon/daemon-pty-runtime-state.ts": "c3424eeeaa1aa8d267df12a6b6b084b6925b45a73cb454f8480d67f8fa564f4e", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7" + }, + "publicationEvaluated": { + "src/main/daemon/degraded-daemon-pty-provider.ts": "a2b95e457bd7819bfe1a56f5dde2fd6785e5a731801d7f75d2f6ed657c9b50cd", + "src/main/daemon/daemon-pty-adapter.ts": "c4868235fea41d1d96b0fb0332b3dc591d4f0a3c98e122e4552c3efbb33791fc", + "src/main/daemon/degraded-daemon-owner-recovery.ts": "74f7b2000d3a637e4b509e29cceebeccb36a791a8fba34bb6e0811cedb7311c6", + "src/main/daemon/degraded-daemon-fallback-shutdown.ts": "12d2cf65777e5ac8d016536503da52f48eaed2f2a297600f5a9746b85f7e6efb", + "src/main/providers/pty-process-inspection.ts": "aad924d84e3ba26c8d5a48102518a9ee0edfa3e47afe0b63537c1a3d92b35b20", + "src/main/daemon/combine-unsubscribes.ts": "4d468d5bdb8b30fceb4044b276f2f50a1cd4e73845562eedf2f0effd4a8532a4", + "src/main/daemon/degraded-daemon-session-routing.ts": "d0cf9bb317fb0e8b1621861db1241c00b3d5e5bcd4574ac6d2c9875960087e6b", + "src/main/daemon/degraded-daemon-fresh-spawn-routing.ts": "c917dfdf1e4cf25b5b3d6249e8307390c4fb2412ebe999d66942403d49ea76ff", + "src/shared/terminal-process-inspection.ts": "790ac705cf1d12bf805c868d558b75073173c59dd1399596f1e7d79e0bcf534b", + "src/main/daemon/daemon-session-owner-resolution.ts": "4a69ddc724e58ab9eb7607a7ed1f740c5a69b0f809148c03a59f623f275fbe62", + "src/main/daemon/types.ts": "ac253a9fe626ab585d7b988df22c1c79b3dd63510c563538ca50876c6dd6f96d", + "src/main/daemon/daemon-pty-listener-emission.ts": "9d1e9b89dfb3bccd2c7e9eb3e0f574fe93772154b57e0ca34ce2779d1b94084f", + "src/main/daemon/daemon-pty-daemon-recovery.ts": "9c76ce16e8d191d542addd0679ed8b730b3eb4c519602b40ffc5705a50c91dce", + "src/main/daemon/daemon-errors.ts": "d6775c72188f400a92e1658e70df76f8b32437ed94d82eee484a404d69a3ffac", + "src/main/daemon/daemon-protocol-version.ts": "9f26d58414fd35891ebc2912cf8e96488b72248dc27f8736dff5d53fd8b27fcd", + "src/main/daemon/daemon-pty-checkpoint-persistence.ts": "2cbe518533013d4208e10c34c40ece45368c11366a9db898fc043c9d19b7fad0", + "src/main/daemon/daemon-health.ts": "c1ba2c5c687bfbf6d99ee82f51b81d9eac238ff90f48af23a086d10ceb2ed114", + "src/main/daemon/daemon-bundle-staleness.ts": "1742a353bedb6cd0685f213feaa5652fbf5ac65d228c52a93426a19c239783c1", + "src/main/daemon/daemon-endpoint-errors.ts": "83a553bd23a971f0f13da5420dfe11dc509f316cb9992153eda2cf09d8290aa7", + "src/main/daemon/daemon-tcc-attribution.ts": "c89d54b3ebe5e2f9c988b39052ddd5e496065515d70a340205b07ac43e7d6923", + "src/main/daemon/daemon-pid-identity.ts": "a64e4cfae8c80b7b83fec38b35f4c3f657e447414056feb435d374d04882a190", + "src/main/daemon/daemon-spawner.ts": "0e247aadfaafdf32f1a866f852ca348dbb481604a20a8990b948889cd9e39017", + "src/main/daemon/daemon-endpoint-ownership.ts": "b2eab15f782c142b756d5562b0246aa4026a7c97a51c3b30e094b607b15f3efc", + "src/main/daemon/daemon-pty-checkpoint-scheduler.ts": "2593cdbd3d0b48e45e04e7205970422bdc6c5ccd240d518961429f88751b50c1", + "src/main/daemon/daemon-durable-history-snapshot.ts": "35c3b071f3cd5f41e6d2f507f029057663983cf42ec211180fabb9f775cc5072", + "src/main/daemon/daemon-pid-file-parse.ts": "9a1acc477a9c76f19b7a2cc167889c74d48006e3538d0669bca5787afdaaad51", + "src/main/daemon/ndjson.ts": "d3df2c0a66ab9b234703d139aec8eea53e512eaf63bd85c97d46610e6de39e91", + "src/main/daemon/daemon-process-start-time.ts": "9cef6be893e1a760357b418863726b64684b7102e3d770dc66290756942372d3", + "src/main/daemon/daemon-process-identity-query.ts": "8f3f051ccbbb251ffb007e5157af73c217ae97f697828b9644c4d512b32afc9b", + "src/shared/main-process-ndjson-framer.ts": "cac5d9cdba42fc9f8340b2b815e6b36ff20e104d734a7ac4b8bca7136e6fa490", + "src/main/daemon/terminal-history-dimensions.ts": "bf8260412fe7e29a38063d2c7355ed63098d4de20a8ca8ebb5d1f9421b73d81b", + "src/main/daemon/cold-restore-replay-writer.ts": "0d9dee4459c802564b1b0ae38a5856d5b5cac98d96723700b08863f23faf05b5", + "src/main/daemon/daemon-respawn-throttle.ts": "976463ced65580f049e9d8b14585faa03dddc280c3cdb095531eb2040a477645", + "src/main/daemon/daemon-restore-scrollback-depth.ts": "ffcf76d820c9dd41289bd0af77d33d073a6c69a89e3ef18cf881b12a91ee6f0d", + "src/main/daemon/headless-emulator.ts": "b8bcb28dae2d9fb1b1361c9d32eb69357177943778bf56c2ce9848c82c9ba2a7", + "src/main/daemon/terminal-shell-lifecycle-scanner.ts": "2bc25f07183c727e5d4e9d5670bbcaa015e37f74abadba422b6bf8f040fdc04c", + "src/main/daemon/daemon-endpoint-probe.ts": "b27023ee842a68593ae657aa9d77cc5f125ee763839af0c1d8e4c8def35e2f52", + "src/main/daemon/daemon-pty-connection-lifecycle.ts": "8f705518dff40fc7fbf847aa310719f2950cd91ce6a2bf320be131ba48992eef", + "src/main/daemon/daemon-request-deadline.ts": "f6a55c176bb4aff63358517b70d69dc187fca406164621385aba4ce43f7f4a10", + "src/shared/process-output-field-scanner.ts": "03f89dcee7b9abc0c8c1a51423cb612ef936b8046d10a6b586fcea05e4a359a5", + "src/main/startup/startup-diagnostics.ts": "7318d5b3ead157615f85f4e5f7e921b64f2222fa01f1450c6c21e09c01b2089e", + "src/shared/terminal-scrollback-policy.ts": "7816c5a7540bbb3fbff61f7b3c4a89a8b331621f769b77bddd29abe06a752f17", + "src/main/daemon/daemon-pty-event-subscriptions.ts": "0a8f8de8430a674e61177563396d0d6322b8d60aa8f182ca8430e5477e4cd16e", + "src/main/daemon/daemon-audit-classifier.ts": "dcf4dff58e0cad4f0159bd1a1592693a920e4b6ef1d5acd92fd953813c5dfe9b", + "src/main/daemon/daemon-listener-registry.ts": "86966184f856fe1367cd3279bd09706e7935dcd62798e1cb8c07b35008281449", + "src/main/daemon/daemon-endpoint-incarnation.ts": "095bcacd5452ad9a893a27ace50b9072a823b291c17aed1901418242d57a2f07", + "src/shared/terminal-unicode-provider.ts": "eae524000d37c01ff9818ebe822dff4c12a3479b9a97370414a928cabb3d948f", + "src/main/daemon/xterm-env-polyfill.ts": "83a5c2f6386d27c9895699fd3dfd1c6bfdb38c6ae24948ea07d75e24c213e6e9", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/shared/terminal-cursor-line-context.ts": "c02e769fb8e49111e63e52079b26feda2d47d70c5e56cf34a81244f2f893cae1", + "src/main/daemon/terminal-osc-cwd-title-scanner.ts": "77145544a185fd5a6007e415ed4c9b7f7bb3f6009c96391a72253f3550b2ea6d", + "src/main/daemon/headless-emulator-modes.ts": "acc05652aa78e3504f3d71250537ac5dbd61777fab9516ed69a22a5d11857807", + "src/main/daemon/terminal-mode-rehydrate-sequences.ts": "922d2533cc78adae2d93850507925426e17f456ea3cfb98bbc03b454b7d766ca", + "src/main/daemon/headless-osc-link-ranges.ts": "90232b09a6aed3f029952a6850f5c3e2037a395ca0c7d2750f497a7f38d1fdca", + "src/main/daemon/terminal-frame-restore-sequences.ts": "828285b5e5ca54ba59e6756d2fc6f89e468016ed3a7595775c6dedd08b2c57c7", + "src/main/daemon/startup-device-attributes-responder.ts": "de1092ba15ec03cdd2fb08b33cd5ba5a04f26b19616476e3c7f717e54d337b7b", + "src/shared/terminal-serialize-absolute-cursor.ts": "5e4566f8b4fb752a8bec0acf9e2e87a36d223b06760d53d911d42ed46b2100a9", + "src/shared/terminal-partial-escape-tail.ts": "0739b887bf650a81b6d596b5d2ec2e17d45254eaf3d95c7d008bd82b4af81d29", + "src/main/daemon/terminal-snapshot-ansi-buffers.ts": "8e95c7e000db648d64a582b06f75a21890c656f391ee996e583bdf7c1f6a13ee", + "src/main/daemon/terminal-view-attribute-responder.ts": "51fa68da93e150a2c15072999b63b3440fb5540b67d732020ca287123ae20fa7", + "src/shared/terminal-mode-reset-profiles.ts": "38028c60950d462412e6ebd9699ef0697c859f27c08da377a16b6c3e5da91049", + "src/main/daemon/osc7-uri-extraction.ts": "6be83d04eef8a38c65df5b8e20513526936a47d2e095e71a0df1269ad93567a7", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/main/daemon/daemon-incarnation-evidence.ts": "be9c8d27d040c0cfc1aca2593fa8a9c823ea6c496fa63bdadeafefa4fd71dabb", + "src/main/daemon/osc7-file-uri.ts": "a09f7b8f2bd73a7d547e5c5eecdaa62e23716a16da05fe082041e771ae8dbe9a", + "src/shared/terminal-view-attributes.ts": "f9a6f84532a49107daadf82cde6ac61949d3881d8ac42327f46a08a0c218e305", + "src/shared/terminal-escape-introducer.ts": "399406e19ea928d9905fe08a7aaa81807c436cda7f55e459293e94e868b43fc5", + "src/main/daemon/daemon-pty-session-inventory.ts": "ef12cd9c10adb536e6884db6e4a00e3915ea2286410228443228fc4972c0ecad", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/main/daemon/daemon-process-inspection.ts": "a5ece8904c6d99621b960d476dbeaddd104bd74e07f85bd2085ee26dc9d69683", + "src/main/agent-hooks/managed-hook-owner-identity.ts": "1c1d48647a1db09d1255b0816b267f3b8d2ec7b1ca267ffbef67b875eedcaf4c", + "src/main/daemon/daemon-incarnation-evidence-types.ts": "85b6135d13257b11ba3162941cd6443ca4e056fdf35091d4ea8f4109f48b1cb8", + "src/main/daemon/daemon-pty-process-inspection.ts": "4104b34ff7b3ef25989c51c391e532d8058b14921fd72216b8be36013f8725a8", + "src/shared/agent-session-host-authority.ts": "8c5952323d2766f136a3685eda51a439d363d1e2224da5516f3d9cabe34bd5d7", + "src/shared/claimed-agent-pty-owner-snapshot.ts": "82935b7bd08639e94ea181be0cb998dfef8c4960eb8d3a8563d6d1aa3156cca5", + "src/main/daemon/pty-session-id.ts": "1c8beaf069eb5e093e45a9489feffac60e8a5635e8f941722d2a3c3124b4f878", + "src/main/providers/pty-process-list-admission.ts": "6ba5c65c9552497aac0182b6ad8b38b4253d7834ee6853c0c6c1c252af7e2356", + "src/shared/claimed-agent-pty-owner.ts": "6c49c9b4649559a45149a3118a3560a7c724d9b06fd9b57182f5b880b4fd77c1", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/pty-session-id-format.ts": "42bb5177724f9dd38ae1814b38f795966536fd803d92364e0b5f9f49a8f8a49b", + "src/main/daemon/daemon-pty-buffer-snapshots.ts": "dd0e66898c3035be4c4fe41138a26f00f2d9d84e3999e328c77aa4cc2f29faf9", + "src/shared/protocol-version.ts": "053a152568959f5576050bc83743924772880c85ab6dfb7ce95c6c6acdb02954", + "src/shared/agent-session-resume.ts": "3dd5cd7ba4c764c848374b168e715a410974657a9b7e70e9d56b69a06254ce43", + "src/shared/stable-pane-id.ts": "0dfb20a173dc658229c46b7df4e36a443fee7089cffb79d76a74d8a92f41b884", + "src/shared/terminal-tab-id.ts": "3bf691ba305880a6d67f620d9ce671e5388e169c4783b1caf0d73fa589b5aa82", + "src/shared/foreground-process-evidence.ts": "e82342503c7fa83197ef1002f306d410ebed43e93c4dea86d6b8643ae87bf53c", + "src/shared/pty-incarnation.ts": "f232a871e24afd92b29e798601f23403a8cc082a6b89e009e5e9d99e6a1f82d5", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/workspace-scope.ts": "961b4e7d05f044aa58ac0f3e0ca9344d166f4787742836c855c06ba90dd71581", + "src/shared/terminal-surface-id.ts": "7093584672a541e22c0094bfb0257f6bd8f83d4b23e1a5b077132da011f8f9da", + "src/shared/remote-server-update.ts": "c7384b964a0bfaa1573cd00761c5e2d29143c591cb1b4cd6ee1b0db464110d12", + "src/shared/skill-install-capability.ts": "4dae209e014524ad9bd1f72f8366a78f80720546d36e19adbc299d3d7da1ff19", + "src/main/daemon/daemon-session-scrollback-window.ts": "038507c6193b5a18eb0e39276d83a1335a33dfb3b87dcf93286ed91a449f9a68", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "src/main/daemon/daemon-pty-session-control.ts": "5fd2e9ad493e0b89cf1c2d18d546e820bdcc863f6c4ba7e95b1dd77f5cea48b8", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/main/daemon/daemon-pty-applied-size.ts": "fc17c9636f0d6ce41ac2e4c45fc093d08a4ce29a9c4a0b6ed7202fc99fb821e0", + "src/main/providers/pty-default-cwd.ts": "ddb07acfd0ee4d45ff62416b013db9c7b2f1db3ef3d3b8120b941d32d71e2f6f", + "src/main/daemon/daemon-pty-lifecycle-errors.ts": "d706e04875eb4c08ae92eb0182eb6f0a4c07150a46b98f817e165dc1add92da1", + "src/main/daemon/daemon-pty-session-input.ts": "c1b08e0c51deebc21f2af16863051976a56565449e16f7aa2660b8d5c8fd6037", + "src/main/daemon/wsl-cold-restore-cwd.ts": "68a2309fc994799667a5955daa654a1bce3d42b4aa9bab28228f7732eb4d4200", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/main/daemon/daemon-pty-size.ts": "cb78f1d5e2cf142f86155595df62fd6f3ccd5f45aa11ed74068efc2de77ab741", + "src/main/providers/pty-path-safety.ts": "c6b67c9756ea26efea2641ec578ad92fdaad3d5f3f4b668aca6133f0f2ec5aa0", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/daemon/daemon-pty-session-spawn.ts": "c14d3697aa509157cd0ccd64bb08dcc258901f3e64d8de85df1d1bbdfd1df114", + "src/main/providers/pty-write-unavailable-error.ts": "bb31714600152bc9e3f0e1be7c81fe50b9301b97f39318245607be3047db8e40", + "src/main/wsl-env.ts": "66eb01ec8a5593bcc0e64089a7024624deb009b19250d3d1e6eab2bbd315be52", + "src/shared/agent-process-recognition.ts": "2a93bfec55fabc7052bf1654d270131a61935c72976bfd3251585c7c1138a0a4", + "src/shared/codex-startup-delivery.ts": "8d2a47a0b56dc6ef1fa4a598973805898bd8e42f566130bd3d301d6cbc5733ad", + "src/main/daemon/session-shell-ready-barrier.ts": "a7ae62cc0ccce6eb8dfceb9437d3c1497d06588043bca49c559f3f6948f64109", + "src/main/daemon/daemon-adoption-telemetry-event.ts": "05ea98bcd32f606cc15c8f0cb9d66401ec2dc801f8e717ed6dd0e9c6911cbd2d", + "src/main/daemon/daemon-pty-spawn-result.ts": "4ef2cd27ee763d3542833e4ebf8afc4e0b75675689295a50e68ca6a79c2efe0e", + "src/main/daemon/shell-ready.ts": "8bea3210928c4efb35de5201a53b370eb473e32c7c404b39778efb1b7f779669", + "src/shared/shell-ready-marker-timing.ts": "4e7fcab88785ecc7d446831d23433c7031a01f534bcd53d5124531b2871857e2", + "src/main/daemon/terminal-history-seed-segments.ts": "e0d1bfc920a0df5a561a8ff6c8227ee352822c3d93f48f1015c271eaa6bafcf5", + "src/main/daemon/wsl-session-context.ts": "860dfd518182937b04e43d8d330066913799b00c4c2a5dbc04f81e7000e4718a", + "src/main/providers/local-pty-utils.ts": "d655643b42503ccfdab6436273557581b447f4b9f452dfa7649a959705744f25", + "src/main/terminal-history.ts": "f4ed054c6d478ae191f1379b614487076eb9ee35ec876197c2c20cc024de3013", + "src/shared/wsl-env.ts": "9f9a6bc39f8a3e4b245338652bf30721d8e3888734a40d7cab602dc792dccc88", + "src/main/telemetry/client.ts": "cd6fd4066a7fab559cc13aa4822ef80f0dba34a1e8211613344223b70db532d0", + "src/shared/app-environment.ts": "91bd2a883048a56f8c3966057a062bac75f48f9e1b5670d8d7958f8c7fe21140", + "src/shared/daemon-adoption-telemetry.ts": "7af34179b683b936ba248b7d92bc0f506642658673b0200e52bdfa4068729ed5", + "src/shared/daemon-lifecycle-telemetry.ts": "b3bcb90f98bbdcf98c76fb985de86b2ac5eb96a4b000c278f5a133bf79bc31dc", + "src/main/wsl.ts": "d888b67769461a7ba0a175db9775e2e2ab30601cbec53077a27351604a04882a", + "src/shared/worktree/id.ts": "d4da9e75db6f0d3301ef5f073d3136282dab4fc4fbd2978bdcdf0e142e7a9d30", + "src/shared/local-windows-terminal-runtime.ts": "6dfb40bf9bb1a73b358615ea82fd86abec10b1a170b7ef0771b9c574addd4e7c", + "src/main/terminal-history-id.ts": "58a0e50e220343925535c67911fb9e45f298916fad2163e6396ba9cc754baac0", + "src/main/terminal-history-paths.ts": "27148b37c6fa2f99540d18864013e8909aa86b65be7f92cd10b6a31a1d3d3e75", + "src/main/fish-history-session.ts": "0f746534888842d53ffc8f5565a6010b1c4a2f7bf5f750301c547f7e5450f8eb", + "src/main/worktree-history-file-path.ts": "b0b04151717e3fb245b17ad8ba140db32e0af55c7c740a14684e3c2729c5f0aa", + "src/main/daemon/daemon-attach-only-retirement.ts": "b883fe6f3647f1a95be9d3f0390653933d8bea79c21538d045fe2085d31bdbca", + "src/main/daemon/daemon-history-recovery-freeze.ts": "e629d2e2729e598293e64213464ca0fb45ba3f205b3f45a45781408f180262a0", + "src/main/daemon/daemon-pty-provider-sequence.ts": "6aa2e70fbc53a57d92b80f1b3e0f8b36ad33d7027191359eb674292d156416bb", + "src/main/daemon/daemon-pty-spawn-request.ts": "d3c2e9f8995c73df427dea36e0ea64508fef6fd5880616df9ad430e34d3acb3e", + "src/main/providers/working-directory-validation.ts": "197bcb7ab254d2bca8cdcc7d7f0214dedca825bbb0dc4f0d2dcbb41df4cec043", + "src/main/providers/macos-tcc-login-shell.ts": "3987924df6e455a41561926d50c3811394232f9dca68d96bbe67c152e57744ba", + "src/shared/node-pty-spawn-helper.ts": "4796707cd3d98c1b296a2b7ff6917b324de6ab830a7bb9cd14eb952bbd587c7d", + "src/shared/command-token-scanner.ts": "86eeb322b12bb3e5bc48edf5d85ec647c0c0ec24d3beb3526e1ae3c9aa7303a0", + "src/shared/agent-headless-command.ts": "10f84a57c243c7e9b13b9cd9419ff174cd0c8b20cf4783aeceb6cebbc52def87", + "src/shared/agent-node-entrypoint-identities.ts": "f2d58fcb086f30afbee3987a3b3ccbde566983e9f230a6da05f9c1d808ffa2d4", + "src/shared/tui-agent-config.ts": "ac7407900e6dafad72cc47aae171b169e8e1d7752d28caa0e09441d73a1fe1ad", + "src/main/shell-prompt-readiness-probe.ts": "8e871ed4a1d61569a29185bfa95ad4aa0a52ca127098088d3853bfcd5a8a2e73", + "src/main/daemon/post-ready-flush-gate.ts": "d53f2d436d768bbe4c4bfe1dd1442e84a031bc8078d3856c0361b40447936cac", + "src/main/shell-startup-output-scanner.ts": "cff0c8234de84ed34c1cbfd9013dbb10f5ac60efa01076ad3be2f80394248ac7", + "src/main/powershell-osc133-bootstrap.ts": "5a53aa934bbb50fba15dc2a0421b07910b09c76824a23ba224d8a4216ef9b446", + "src/main/daemon/daemon-shell-ready-marker.ts": "d128c88e65e7835864c67dc2c1e1a355c50111de77802930b06b350a0beb9a21", + "src/main/pty/codex-shell-launch-preflight.ts": "d80794841adace87db9a24fe3d73f873d5a1b9219ac6887962bace681677deca", + "src/main/shell-wrapper-file-writer.ts": "5ebfaf5c3356c651a359b6064cc8aac4a32a413b86548681b1e20370e198f232", + "src/main/daemon/daemon-shell-ready-wrapper-fileset.ts": "09cb0a1ff975c2299c46ea47c4ed79820aa93fc505592875db4add634249c55b", + "src/main/shell-wrapper-content-address.ts": "bd1ddf63b4de77fe8dabf533edfb0a14b81416e114f795978a1a46776ecea028", + "src/main/zsh-wrapper-dir-ownership.ts": "e6544e2d8d3348cc2a2e533c60a2bd1e91b6c8446fffa4dea24968218f9e2c9e", + "src/main/shell-templates.ts": "17122cebe3ea2b01462e645fa3ce335495734a3ab18f5fb7d015180060896d0a", + "src/main/shell-startup-features.ts": "1b418a6d759230950f320494fa70dddea030d3d1f29ea1fba05c238fc98211c7", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/main/telemetry/validator.ts": "39e028a8e431fe991ad6bf37acad7a0b0e9d7c174c07e898779b4a614bd12c99", + "src/main/telemetry/burst-cap.ts": "12a72a3f1582210fb8b8a81818f1471d62938359f97bdc2e5a2ce59571a3ce2c", + "src/main/telemetry/cohort-classifier.ts": "2d9aeafab4782fd350622d122d0381dda6b02272eda4db0d10b148bf2d5c933f", + "src/main/telemetry/consent.ts": "dd8b489ed5ed4af5cc912182ea1ec165f8e0056260a2f3b1e6725aa025027d39", + "src/main/shell-startup-identity-scanner.ts": "37aa9dc4a6ed70c5b52aeda7c4204cffc5b0838808dbe65df4266033628088f6", + "src/shared/print-mode-headless-command.ts": "4a632179d794718904989bfb00dabb7c741a0ae35d58585cf07f4aa746c36f7a", + "src/shared/priority-semaphore.ts": "509693d0132f2365bf1702bcbae7efc4bb0eceeb6ea235456f56a5f784da85a5", + "src/main/shell-ready-marker-scanner.ts": "a504be037d0ef1a19d3ce1ad608c279028132f0c91357c6dc1f2d5486d914a4d", + "src/shared/ante-headless-command.ts": "710a59e55ab1dfa32a33f7df9b4ecac77f85e387dc93e740d0bc678c37e61620", + "src/main/daemon/terminal-history-seed-chunks.ts": "a2616cd2815d92ba8dbe9ddad9d34adc08076a33765010ee18e82c4905adee3f", + "src/main/wsl-directory-probe-command.ts": "1db18370dff7e8656d27b105625dd03bdba51cb60099e90c0abb3252ec2ec246", + "src/main/daemon/daemon-pty-runtime-state.ts": "d0898287992c4a3acb14c9c4087c7b28cf11b13417f115d25b445d25d1404f19", + "src/main/providers/macos-login-session-pty-probe.ts": "260cb5a780287b6dea7c1eaa45e89b78fb9a5e1805c1f8c4bf2b17c5d952af63", + "src/main/wsl-availability.ts": "324c636f9779595b62fe1f8c98535ddc818b6d892195f1f28ec2ae0bd7ec1bcf", + "src/main/wsl-interop-spawn-directory.ts": "857a1253971cb0d4e7da5659c9ded80c4f0930c70b03e243ed858f8999f294cf", + "src/main/wsl-running-distro-cache.ts": "d8bcf954daa78cba331ef725bf1ab2c63a117cb3d0bb099edba6133ffeee386a", + "src/shared/child-process/run-process.ts": "ae4dad239273351ce61269cd7f7abb8325f3f620be600fa9c0ae760c1dd7cbad", + "src/main/wsl-distro-retry.ts": "d5711a29eb67bd49d9c512a5f35743921045b703ba27275cdcdc0d5aaf64e4be", + "src/main/wsl-distro-list-output.ts": "27f772c9dc451757f4397a4e47ca9c9e28c4067e50a41ceef846013ff43db7bc", + "src/main/line-editor-ready-output-scanner.ts": "c697380e4bf4677229f59346d7e69d7cc27ed03d163a2882885092e6eb703293", + "src/shared/pty-slave-line-discipline-echo.ts": "3df5bc2c7c45ffce766da5460ed5d888f463768d8d8287792cecf45e5f3618b4", + "src/shared/shell-process-readiness.ts": "9aeb7bcdc6e0ca1b70d62471a8ad7ac5f39fab966134cfd2397a8d7f850d82cd", + "src/shared/prime-agent-headless-command.ts": "0898cb5322ff91510e81c11ec9c7d4fc1cd6e850491092a78f79931722651ec3", + "src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts": "502ef8934f77c6ec3ffe278cc4f8e644f0890bc4f2ac6360077e34c00bc3d58b", + "src/main/zsh-startup-wrapper-builder.ts": "eacff52ff47cebcd34914b020fa746d1e3720e3b7c8059013ffcf74d1cf3dc26", + "src/main/daemon/daemon-bash-shell-ready-rcfile.ts": "475bdef878015fb3531fe0f340d0f99d224146f0aa8adde406175a2e51e228be", + "src/main/bash-prompt-command-composition.ts": "1519e1b2f4d408c5e46ff7f7d9b3dc2187ac03ed6e99217ef2b5137158fa264d", + "src/main/pty/posix-shell-startup-command.ts": "f69611a77938cf13455f67728cf1a9fc1a67ff864919245787d659f3c1c6055c", + "src/main/cli/bundled-cli-launcher-path.ts": "d18971de3491d0f678c99ab3fca72dd71c5b3e635bf258b7c8d1ba4584e358c4", + "src/shared/powershell-command-encoding.ts": "405d7aa5cf55f0053a6ae84f92335446dde6c316f782761be5ce1c24967046a9", + "src/shared/orca-cli-command-name.ts": "c90b037ec7a6143770a40dd907bf9d053fb2cede21128b60b78a78671c0780c0", + "src/main/pty/omp-shell-wrapper.ts": "fcf15117265833933064f0315ce1b02473d8f5618efed09c0d3f8d4872395fcf", + "src/shared/telemetry-events.ts": "40b800fdb343cd66801613dddfcacebc148adf864ceb65e6f70e250136e9f4b6", + "src/main/daemon/daemon-audit-eligibility-event.ts": "11755c26094c81a82c72027eb5b96b97fbd686bca4f32c9fbc9ba1758fbfa137", + "src/main/daemon/client.ts": "e7698849f62c51bd36bf6789423dcd3f9194d979fc143dd267f0c1b1de1ddf14", + "src/main/daemon/daemon-checkpoint-session-queue.ts": "f4cc3809309c57125955ee8a7439df8f8000bfca452015c5bffad3ceea5fdf7b", + "src/main/daemon/cold-restore-payload-cache.ts": "6e97bb387ec9b00619226685934126a90e4572720de3ed6a16de0cf8f43ceb77", + "src/main/daemon/history-manager.ts": "7b0a99ecbf735ea3bf6480af6463dc15a35ea6dc27b3b0e8d07b73a787ad0f5c", + "src/shared/process-table-snapshot.ts": "fe423af1745925a52b5d205a3cb20325220c39996e1c065d56fe2af1e69182e9", + "src/main/daemon/history-reader.ts": "8579629f0d023bad3d652661c6f767ad5be767a2759c2978b114645df0814a3d", + "src/shared/child-process/spawn-resolution.ts": "8c62c3c14d6f110f25bab23e3f4b2633a68e2f36252bd3aac68cffd7b2e274dc", + "src/shared/child-process/process-tree-termination.ts": "49f001659c872e23040fe5bab2dd387f2589178f5bc3ce9361373303d23ff120", + "src/shared/child-process/bounded-output-sink.ts": "7dbab9fc70dac8e5d19dd03d743b55609092b45ca13423e2dc80a69cf7fc4865", + "src/shared/child-process/process-spec.ts": "97e52b5e320da1eed4cfb2c0b73a88696b62f355eb4b5fe0df5389764afb8289", + "src/shared/child-process/child-termination-reporter.ts": "d91ff09b26c9a23e26c2a2f92b129879cd6d0bddea14cd3bcac5d4489fecfca0", + "src/shared/wsl-login-shell-command.ts": "69445d4cb6eae151ef28ed4fe320774bad9c46577f8a2263c14a14ecfdb28075", + "src/shared/telemetry-event-classification.ts": "0a04ffab3f99083e2a07e254040633519771450303e6dc803cd46ac074c97a4e", + "src/shared/telemetry-property-schemas.ts": "3b09098052708589b2954189e19d595668b4e3d4ccb09b1de75681839d7c72bd", + "src/shared/telemetry-app-event-schemas.ts": "5dc7d68e3a235db3755ae71381a8b01e3584f21a80b6de48a6b4e7538f1c0831", + "src/shared/telemetry-event-registry.ts": "fd2782c8606fac43241fa96e0999fd651047434ffac68688dd009f060a315be9", + "src/shared/telemetry-daemon-event-schemas.ts": "0566f2763169d68a63887bcabf15e3f6a92237191caeaa01276022c647c982ac", + "src/shared/child-process/process-tree-kill-gate.ts": "62848f063785e15cb0231deb622b043e81484ed3040c612afdc4f98fa2db06ab", + "src/main/daemon/history-paths.ts": "a565f91fb2def34aa2c063d0089fa9b491c5aabd7736886ba9e4d2bb31418b63", + "src/main/daemon/daemon-client-hello-handshake.ts": "532be287024d5fa8374d61af5ed488622b2f7a8530642a2120a4993caa14cc73", + "src/main/daemon/daemon-client-pending-requests.ts": "4fa6740fc377c898493809f7469b98c37062c059aaef38bd07dcd6f5e9bccb9b", + "src/main/daemon/daemon-client-socket-connect.ts": "958e918424e5ea6d9ed4f8d85e1d07c46977e074ea2f2f880dc6b1ac5c927c76", + "src/main/daemon/daemon-client-listener-registry.ts": "ff74d77bd915b4ae424054be740322e17e46bcc64316057c264eed1010c1a50b", + "src/main/daemon/daemon-client-ndjson-readers.ts": "3333a83f0f75a05867083c6890c564e4bdbd0df820313d646944c55ba04056ee", + "src/main/daemon/daemon-client-notify-settlement.ts": "7ce27ff7c79fb5027b884328505f0b792c4a039886deae12371a4b5e4594bf25", + "src/shared/child-process/windows-command-line.ts": "4bf94aa90ee63d965bc6e1198ddafa4a715c05d78be979025d17dc59d90557ca", + "src/shared/child-process/windows-cmd-shim-resolution.ts": "5044e3a9140a19c8bb713044de655b611244659a754f32e5a5df7f519df40eb3", + "src/main/daemon/daemon-client-rpc-request.ts": "c846c4e7cb1a5eaa3266e3f3779dbd62c7f60a0c783c55d2af80c42d8468db7b", + "src/main/daemon/terminal-history-log.ts": "8d9ec783257c1f7bd7380491038b499ecaca00f57d99047806cba9d6ab816bcb", + "src/main/daemon/terminal-history-checkpoint-reader.ts": "d69e6e765d9b318ecbdeca27fc295e57e678d47e1312b77f0b2900b1b727b8d1", + "src/main/daemon/terminal-history-file-reader.ts": "923c9e0c5e803c2f3ea25969341c76525ece822ddd3cea4d96c5176c438b92ae", + "src/main/daemon/terminal-history-session-tombstone.ts": "ed4ced675f275e16b5a1a142e91b5e33e10a4a35885ba2dd8a7e5a90e4d8ebe6", + "src/main/daemon/terminal-history-recovery-quarantine.ts": "65e37d29a84d8ebaf43e78e13b94004c2ca9a0801c49a843642ff84928837c85", + "src/main/daemon/terminal-history-cold-restore-info.ts": "5f44e06414227fa7148c0a88784aa5146b23ab36719db9d293432ebde63b0680", + "src/main/daemon/terminal-history-file-limits.ts": "21185a571e0d326472554dae3b49bc578834d00432a2bf50f044a4d3da1e1a3a", + "src/main/daemon/terminal-history-restorable-retention.ts": "1bfc897132026875086cb99e9d2d5df73db793cd6cef916706934b215bfd7b7d", + "src/main/daemon/terminal-history-legacy-scrollback-restore.ts": "5af2cb2998362feccbe1025e019aef6c5ebaf1d4ae9d9e4c6ef527033f1fc441", + "src/main/daemon/terminal-history-metadata.ts": "d273daac2c06d140b8a75a1d43382025f04b430708af57fd359623ee506ce084", + "src/main/daemon/terminal-history-mutation-tracker.ts": "fa7a88fb2bd9461b12cd97c60c1383f9c51b4d0e445b9d8852dbeabf136f548e", + "src/main/daemon/terminal-history-recovery-freezes.ts": "c1dd816085442d69b556a590196bed679aaf02f14d919ca7c62742d382591f0a", + "src/main/daemon/terminal-history-session-files.ts": "d8b0c2ac3e41a267f2a85aa11571e66807e9204613881433ddeb2a5beca217fb", + "src/main/daemon/terminal-history-session-writer.ts": "50eb181f8f13355e9ebf5c3dd1bc004ec10b3eba51bd74368a75421634562856", + "src/main/daemon/daemon-private-file-modes.ts": "7dbd0693d254e8ec2bf4f73499ecc8eca560ef1d440fe0026cff910e58bf0615", + "src/shared/star-nag-telemetry.ts": "dd3e57bc92a680ca78c521d3e503ec42220c7c9bef6970523cb804e79a7f0f7b", + "src/shared/telemetry-feature-education-event-schemas.ts": "51d0e9260f7945aa8efe28c0db73b935ff662da03567e6867f1c5470e24cd2fe", + "src/shared/telemetry-native-feature-event-schemas.ts": "407d1e75d18cde42ef430689af476d122b2f3f6a08b4c406454f1b0bd66c453d", + "src/shared/telemetry-onboarding-event-schemas.ts": "9e74c1de5cb4cc07b5f6cd0ec3e03fc3c183ef16e862e2485217057397b28756", + "src/shared/telemetry-repository-event-schemas.ts": "d8cc2d46954a41accc3b5e5cecefc6be2410e60e416411e5be8178cd13cc2572", + "src/shared/daemon-audit-eligibility.ts": "54322c582a2ffc880ace5b0eefd3f44b8110e038de9603207601d9ef35979446", + "src/shared/feature-wall-tour-depth.ts": "93614de5dacfa1fd084dffbe56d8a6a5a20097dc6fbc6c39eabd7f49e60e46cb", + "src/shared/setup-script-import-providers.ts": "f152cefab4922d992633da4d950b319b9e61862c3835d6fe88ed0e98edcc3f5a", + "src/shared/workspace-source.ts": "78c66dc7361ade7984de2d7fb5afa879eaade2a2f289e42cd78e53eee07d5b29", + "src/shared/gh-star-source.ts": "92d6db38e5f5881d9c522fa0e85c9f4b757e41cc92517e5d642c8795eb4310fe", + "src/shared/feature-interactions.ts": "d0f06fea1a436f77584b8bc3c93a944ee0cc1504cb0b12fc08cf49eb2d6dec7a", + "src/shared/agent-hook-types.ts": "dc64ba21ad8dd32c4a9a43eacb2c66d19c6dfe8dc6d77b2656f008b70597b78b", + "src/main/daemon/node-pty-error-hints.ts": "0595cf09081e0ed369078d1698093f02f3c23e5814e3d884b5083e70e8bd440a", + "src/shared/terminal-owner.ts": "60e3c07e99b8d2813f2241a8fc5ab8d1a11229b23b32aa53fb67154052c92309", + "src/shared/terminal-osc-link-ranges.ts": "bc3c39fd44489a27669225474ab9f52aaae32988ed80b26aab69e1ad0cb3d390", + "src/shared/node-bounded-file-reader.ts": "2cffa451b72d059226e0c2d4b6d2a5203cb8ddafb358d5e828dcd3436d197ecb", + "src/main/daemon/terminal-checkpoint-serializer.ts": "dfd5f0f3b72f58ad9d6b9df712dc1318b8e04dc7ec7327ccf714c0e7bfde1375", + "src/main/host-tree-removal.ts": "ba8ea5e3503bf30eddf70efc68bdd6a1d9b864734ac67cf21f83b95f4f862c97", + "src/shared/nested-repo-telemetry.ts": "fdcb4ce4565e7d71f6833099c3725a7bb2fcf27ef6d2089ad77ba588ad12efe7", + "src/shared/feature-interaction-categories.ts": "320daec20570a0d5cae909bd4eec1d8b35a460a54aee480ddfb464fc1603d27d", + "src/shared/feature-interaction-catalog.ts": "0015a3cfc624d9dd800d54dba6cc420e4e7505131cedc3314f38c1ed03eeda80", + "src/shared/feature-interaction-usage-buckets.ts": "5d4ac4b615d4af1dbb3057d3373d33fdf25c4bef15f93a93884c3e660cfcc8ff", + "src/shared/feature-wall-telemetry.ts": "1ae90c3ee862a109493eb88ff3690a124d05e3c5cdaac331b818610925ec5693", + "src/shared/feature-wall-setup-steps.ts": "e917ed94cf96ea99cce9f537bdd37bd0ddb7cbc6172d8f67aac9efe11379160d", + "src/shared/telemetry-onboarding-foundation-schemas.ts": "32772dc9111ad9e72f83322ee54a4f090b49cd14592f95b129be61b6469dc0d3", + "src/shared/feature-education-telemetry.ts": "7330076fabc31fc2878b79caa7b2b1cc901d6f99387e7ec846a85942f08b4d00", + "src/shared/windows-transient-lock-removal.ts": "dbfbc53d044d4bb76d1c5378a1011dff69b88c1ba042b23ebd1b0057153868f2", + "src/main/asar-transparent-fs.ts": "48b031ef8f72545c5f8f0d4cf6fb80e168a38315e83103ff00e4392c9cbdd2ee" + } +} diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/sources.cjs b/docs/audits/daemon-shared-owner-incarnation-retention/sources.cjs new file mode 100644 index 00000000000..f1a0346cfd9 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/sources.cjs @@ -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 } diff --git a/docs/audits/daemon-shared-owner-incarnation-retention/validation.json b/docs/audits/daemon-shared-owner-incarnation-retention/validation.json new file mode 100644 index 00000000000..f9e7ea8a662 --- /dev/null +++ b/docs/audits/daemon-shared-owner-incarnation-retention/validation.json @@ -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." +} diff --git a/src/main/daemon/daemon-session-owner-resolution.ts b/src/main/daemon/daemon-session-owner-resolution.ts index 1906ebfb35b..47fe531ec7f 100644 --- a/src/main/daemon/daemon-session-owner-resolution.ts +++ b/src/main/daemon/daemon-session-owner-resolution.ts @@ -52,6 +52,12 @@ export class DaemonSessionOwnerResolver { this.routeIncarnations.delete(sessionId) } } + // 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) + } + } } async spawnAttachOnly(opts: PtySpawnOptions & { sessionId: string }): Promise { diff --git a/src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts b/src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts new file mode 100644 index 00000000000..2fb980f1870 --- /dev/null +++ b/src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts @@ -0,0 +1,157 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { DaemonPtyAdapter } from './daemon-pty-adapter' +import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider' +import { LocalPtyProvider } from '../providers/local-pty-provider' +import type { PtyProcessInfo } from '../providers/types' +import { TerminalSessionOwnerUnverifiedError } from './daemon-errors' + +const cleanups: (() => void)[] = [] + +afterEach(() => { + for (const cleanup of cleanups.splice(0)) { + cleanup() + } + vi.restoreAllMocks() +}) + +function adapterFixture(label: string, pid: number) { + const adapter = new DaemonPtyAdapter({ + socketPath: join(tmpdir(), `unused-${label}.sock`), + tokenPath: join(tmpdir(), `unused-${label}.token`) + }) + const client = adapter['client'] + vi.spyOn(client, 'ensureConnected').mockResolvedValue() + vi.spyOn(client, 'ensureConnectedWithin').mockResolvedValue() + const identity = vi.spyOn(client, 'getDaemonIdentity') + const request = vi.spyOn(client, 'request').mockResolvedValue({ sessions: [] }) + const spawn = vi.spyOn(adapter, 'spawn').mockImplementation(async (opts) => ({ + id: opts.sessionId ?? 'unexpected-fresh-spawn', + incarnationId: opts.expectedIncarnationId, + isReattach: true + })) + return { + adapter, + request, + spawn, + setProcesses(processes: PtyProcessInfo[]) { + request.mockResolvedValue({ + sessions: processes.map((process) => ({ + sessionId: process.id, + incarnationId: process.incarnationId, + cwd: process.cwd, + isAlive: true + })) + }) + }, + async publishIdentity(generation: number) { + identity.mockReturnValue({ + pid, + startedAtMs: generation + 1, + launchNonce: label + generation + }) + await adapter.establishLifecycleLease() + } + } +} + +async function fixture() { + const current = adapterFixture('current', 999_999_997) + const legacy = adapterFixture('legacy', 999_999_998) + const fallback = new LocalPtyProvider() + vi.spyOn(fallback, 'listProcesses').mockResolvedValue([]) + const provider = new DegradedDaemonPtyProvider({ + current: current.adapter, + legacy: [legacy.adapter], + fallback + }) + cleanups.push(() => provider.dispose()) + await current.publishIdentity(0) + await legacy.publishIdentity(0) + const recovery = provider['ownerRecovery'] + return { current, legacy, provider, recovery } +} + +function processInfo(id: string, incarnationId: string): PtyProcessInfo { + return { id, incarnationId, cwd: '', title: 'shell' } +} + +describe('shared daemon owner incarnation retirement', () => { + it('releases both private indexes after repeated authenticated daemon replacements', async () => { + const { current, legacy, provider, recovery } = await fixture() + legacy.setProcesses([processInfo('legacy-live', 'legacy-incarnation')]) + for (let generation = 0; generation < 16; generation++) { + const id = `retired-${generation}` + current.setProcesses([processInfo(id, `incarnation-${generation}`)]) + await provider.discoverDaemonSessions() + await expect(provider.probePtyLiveness(`unmapped-${generation}`)).resolves.toBe(false) + expect(recovery['livenessResolver']['routeIncarnations'].get(id)).toBe( + `incarnation-${generation}` + ) + current.setProcesses([]) + await current.publishIdentity(generation + 1) + expect([...provider['sessionProviders'].keys()]).toEqual(['legacy-live']) + expect([...recovery['attachResolver']['routeIncarnations'].keys()]).toEqual(['legacy-live']) + expect([...recovery['livenessResolver']['routeIncarnations'].keys()]).toEqual(['legacy-live']) + } + }) + + it('preserves the same session ID after another provider publishes its successor', async () => { + const { current, legacy, provider, recovery } = await fixture() + current.setProcesses([processInfo('same-id', 'old-incarnation')]) + await provider.discoverDaemonSessions() + await provider.probePtyLiveness('unmapped-old') + current.setProcesses([]) + legacy.setProcesses([processInfo('same-id', 'new-incarnation')]) + await provider.discoverDaemonSessions() + await provider.probePtyLiveness('unmapped-new') + await current.publishIdentity(1) + expect(provider['sessionProviders'].get('same-id')).toBe(legacy.adapter) + expect(recovery['attachResolver']['routeIncarnations'].get('same-id')).toBe('new-incarnation') + expect(recovery['livenessResolver']['routeIncarnations'].get('same-id')).toBe('new-incarnation') + }) + + it('keeps matching-incarnation direct attach without consulting another inventory', async () => { + const { current, legacy, provider } = await fixture() + legacy.setProcesses([processInfo('live', 'live-incarnation')]) + await provider.discoverDaemonSessions() + await provider.probePtyLiveness('unmapped') + await current.publishIdentity(1) + current.request.mockClear() + legacy.request.mockClear() + await expect( + provider.spawn({ + sessionId: 'live', + attachOnly: true, + cols: 80, + rows: 24, + expectedIncarnationId: 'live-incarnation', + expectedIncarnationIsAuthoritative: true + }) + ).resolves.toMatchObject({ id: 'live', incarnationId: 'live-incarnation', isReattach: true }) + expect(current.request).not.toHaveBeenCalled() + expect(legacy.request).not.toHaveBeenCalled() + expect(current.spawn).not.toHaveBeenCalled() + expect(legacy.spawn).toHaveBeenCalledOnce() + }) + + it('retains authoritative incarnation mismatch refusal after another daemon changes', async () => { + const { current, legacy, provider } = await fixture() + legacy.setProcesses([processInfo('live', 'current-incarnation')]) + await provider.discoverDaemonSessions() + await current.publishIdentity(1) + await expect( + provider.spawn({ + sessionId: 'live', + attachOnly: true, + cols: 80, + rows: 24, + expectedIncarnationId: 'retired-incarnation', + expectedIncarnationIsAuthoritative: true + }) + ).rejects.toBeInstanceOf(TerminalSessionOwnerUnverifiedError) + expect(current.spawn).not.toHaveBeenCalled() + expect(legacy.spawn).not.toHaveBeenCalled() + }) +}) From 78289d8ebe5584508750617caed011f0eacd16c6 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:12:34 -0700 Subject: [PATCH 21/59] fix: release settled browser results after dispatcher close (#21164) Co-authored-by: m4air --- .../browser-closed-result-retention/README.md | 77 ++++ .../before-electron-results.json | 138 ++++++ .../before-node-results.json | 138 ++++++ .../browser-closed-result-retention/fix.patch | 16 + .../fixed-electron-results.json | 138 ++++++ .../fixed-node-results.json | 138 ++++++ .../scenario.test.mjs | 330 ++++++++++++++ .../source-versions.json | 405 ++++++++++++++++++ .../sources.cjs | 42 ++ .../validation.json | 228 ++++++++++ .../vitest.config.mjs | 30 ++ .../browser-client-host-command-dispatcher.ts | 3 +- ...rowser-client-host-command-result-cache.ts | 6 +- ...wser-client-host-command-retention.test.ts | 201 +++++++++ 14 files changed, 1888 insertions(+), 2 deletions(-) create mode 100644 docs/audits/browser-closed-result-retention/README.md create mode 100644 docs/audits/browser-closed-result-retention/before-electron-results.json create mode 100644 docs/audits/browser-closed-result-retention/before-node-results.json create mode 100644 docs/audits/browser-closed-result-retention/fix.patch create mode 100644 docs/audits/browser-closed-result-retention/fixed-electron-results.json create mode 100644 docs/audits/browser-closed-result-retention/fixed-node-results.json create mode 100644 docs/audits/browser-closed-result-retention/scenario.test.mjs create mode 100644 docs/audits/browser-closed-result-retention/source-versions.json create mode 100644 docs/audits/browser-closed-result-retention/sources.cjs create mode 100644 docs/audits/browser-closed-result-retention/validation.json create mode 100644 docs/audits/browser-closed-result-retention/vitest.config.mjs create mode 100644 src/main/browser/browser-client-host-command-retention.test.ts diff --git a/docs/audits/browser-closed-result-retention/README.md b/docs/audits/browser-closed-result-retention/README.md new file mode 100644 index 00000000000..4658aedfbb0 --- /dev/null +++ b/docs/audits/browser-closed-result-retention/README.md @@ -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:77–79`). `close` aborts active work and cancels queued work, but retains its pages and cached completed records when the join returns false (`:156–179`). `finishHandler` clears those owners only after the last native handler settles (`:268–272`). A newly settled cancellation record is also cached while a sibling remains active (`:297–302`). + +`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:27–55`). + +Defaults (`browser-client-host-command-state.ts:7–13`) 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,89–99,116–131`). 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:165–180`). If close times out, actual `closeBrowserClientHostComposition` defers executor close behind `whenHandlersSettled` (`paired-runtime-browser-client-host-teardown.ts:37–56`). 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:20–40`). The actual registry delegates to `navigateBrowserRouteGuest`, which awaits native `guest.loadURL` (`browser-route-guest-lifecycle.ts:99–123`) 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:42–57`; startup `main-process-ready-runtime.ts:61–77`). Core handlers such as browser.snapshot destructure runtime and call its method without observing that signal (`runtime/rpc/methods/browser-core.ts:39–43`). +- 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:20–36`), 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:34–43`). 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. diff --git a/docs/audits/browser-closed-result-retention/before-electron-results.json b/docs/audits/browser-closed-result-retention/before-electron-results.json new file mode 100644 index 00000000000..41e67e61037 --- /dev/null +++ b/docs/audits/browser-closed-result-retention/before-electron-results.json @@ -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 + } + ] +} diff --git a/docs/audits/browser-closed-result-retention/before-node-results.json b/docs/audits/browser-closed-result-retention/before-node-results.json new file mode 100644 index 00000000000..69475f07d70 --- /dev/null +++ b/docs/audits/browser-closed-result-retention/before-node-results.json @@ -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 + } + ] +} diff --git a/docs/audits/browser-closed-result-retention/fix.patch b/docs/audits/browser-closed-result-retention/fix.patch new file mode 100644 index 00000000000..1213361b4e3 --- /dev/null +++ b/docs/audits/browser-closed-result-retention/fix.patch @@ -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 ++ } diff --git a/docs/audits/browser-closed-result-retention/fixed-electron-results.json b/docs/audits/browser-closed-result-retention/fixed-electron-results.json new file mode 100644 index 00000000000..7bcb23d2fcd --- /dev/null +++ b/docs/audits/browser-closed-result-retention/fixed-electron-results.json @@ -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 + } + ] +} diff --git a/docs/audits/browser-closed-result-retention/fixed-node-results.json b/docs/audits/browser-closed-result-retention/fixed-node-results.json new file mode 100644 index 00000000000..f30dea10cf7 --- /dev/null +++ b/docs/audits/browser-closed-result-retention/fixed-node-results.json @@ -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 + } + ] +} diff --git a/docs/audits/browser-closed-result-retention/scenario.test.mjs b/docs/audits/browser-closed-result-retention/scenario.test.mjs new file mode 100644 index 00000000000..8e1bab9406f --- /dev/null +++ b/docs/audits/browser-closed-result-retention/scenario.test.mjs @@ -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 }) +}) diff --git a/docs/audits/browser-closed-result-retention/source-versions.json b/docs/audits/browser-closed-result-retention/source-versions.json new file mode 100644 index 00000000000..3942a5e7a71 --- /dev/null +++ b/docs/audits/browser-closed-result-retention/source-versions.json @@ -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" +} diff --git a/docs/audits/browser-closed-result-retention/sources.cjs b/docs/audits/browser-closed-result-retention/sources.cjs new file mode 100644 index 00000000000..c05aa714517 --- /dev/null +++ b/docs/audits/browser-closed-result-retention/sources.cjs @@ -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 } diff --git a/docs/audits/browser-closed-result-retention/validation.json b/docs/audits/browser-closed-result-retention/validation.json new file mode 100644 index 00000000000..d793de73f51 --- /dev/null +++ b/docs/audits/browser-closed-result-retention/validation.json @@ -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 ", + "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." + } +} diff --git a/docs/audits/browser-closed-result-retention/vitest.config.mjs b/docs/audits/browser-closed-result-retention/vitest.config.mjs new file mode 100644 index 00000000000..6622fc9269b --- /dev/null +++ b/docs/audits/browser-closed-result-retention/vitest.config.mjs @@ -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 diff --git a/src/main/browser/browser-client-host-command-dispatcher.ts b/src/main/browser/browser-client-host-command-dispatcher.ts index b79b0cbdcad..54140ff9585 100644 --- a/src/main/browser/browser-client-host-command-dispatcher.ts +++ b/src/main/browser/browser-client-host-command-dispatcher.ts @@ -163,6 +163,7 @@ export class BrowserClientHostCommandDispatcher { for (const page of this.pages.values()) { page.retiring = true this.cancelPage(page, 'browser_host_command_cancelled') + this.resultCache.releasePage(page) } const settled = await joinBrowserClientHostCommands( [...this.pages.values()].flatMap((page) => @@ -297,7 +298,7 @@ export class BrowserClientHostCommandDispatcher { private removeActiveRecord(page: PageState, record: CommandRecord): void { if (removeActiveCommandRecord(page, record)) { this.activeCommands -= 1 - this.resultCache.record(page, record) + this.resultCache.record(page, record, !this.closed) } } diff --git a/src/main/browser/browser-client-host-command-result-cache.ts b/src/main/browser/browser-client-host-command-result-cache.ts index 0a3796e6a92..59c6430244c 100644 --- a/src/main/browser/browser-client-host-command-result-cache.ts +++ b/src/main/browser/browser-client-host-command-result-cache.ts @@ -8,7 +8,11 @@ export class BrowserClientHostCommandResultCache { private readonly maxTotal: number ) {} - record(page: PageState, record: CommandRecord): void { + record(page: PageState, record: CommandRecord, retain = true): void { + if (!retain) { + this.evict(page, record.event.commandSequence, record) + return + } page.settledSequences.push(record.event.commandSequence) this.pagesByRecord.set(record, page) while (page.settledSequences.length > this.maxPerPage) { diff --git a/src/main/browser/browser-client-host-command-retention.test.ts b/src/main/browser/browser-client-host-command-retention.test.ts new file mode 100644 index 00000000000..1b00a40c330 --- /dev/null +++ b/src/main/browser/browser-client-host-command-retention.test.ts @@ -0,0 +1,201 @@ +import { expect, it } from 'vitest' +import type { + BrowserClientHostCommandEvent, + BrowserClientHostCommandResult, + BrowserClientHostLeaseAuthority +} from '../../shared/browser-client-host-protocol' +import { BrowserClientHostCommandDispatcher } from './browser-client-host-command-dispatcher' +import { BrowserClientPageCommandExecutor } from './browser-client-page-command-executor' +import { createCommand, createHarness } from './browser-client-page-command-executor-test-harness' +import { closeBrowserClientHostComposition } from './paired-runtime-browser-client-host-teardown' + +const authority: BrowserClientHostLeaseAuthority = { + authorityRuntimeId: 'runtime-a', + authorityEpoch: 'epoch-a', + browserHostClientId: 'client-a', + browserHostGeneration: 3, + pageCommandProtocolVersion: 1 +} + +function deferred(): { promise: Promise; resolve: (value: T) => void } { + let resolve = (_value: T): void => {} + const promise = new Promise((settle) => { + resolve = settle + }) + return { promise, resolve } +} + +async function collect(): Promise { + if (!global.gc) { + throw new Error('This retention test requires --expose-gc') + } + for (let turn = 0; turn < 8; turn += 1) { + await new Promise((resolve) => setImmediate(resolve)) + global.gc() + } +} + +function command( + sequence: number, + body: BrowserClientHostCommandEvent['command'], + page = 'page-a' +): BrowserClientHostCommandEvent { + return createCommand('createPage', { + browserPageId: page, + commandSequence: sequence, + commandId: `${page}-${sequence}`, + command: body + }) +} + +async function rememberResult( + dispatcher: BrowserClientHostCommandDispatcher, + sequence: number +): Promise> { + const result = await dispatcher.dispatch( + command(sequence, { type: 'automation', method: 'browser.snapshot', params: {} }) + ) + if (result.status !== 'completed' || typeof result.value !== 'object' || !result.value) { + throw new Error('Expected an object result') + } + return new WeakRef(result.value) +} + +function dispatchInput( + dispatcher: BrowserClientHostCommandDispatcher, + page: string +): { input: WeakRef; result: Promise } { + const params = { title: `small-input-${page}` } + return { + input: new WeakRef(params), + result: dispatcher.dispatch( + command(2, { type: 'automation', method: 'browser.snapshot', params }, page) + ) + } +} + +it('releases completed results on close while preserving pending native page custody', async () => { + const harness = createHarness() + const navigation = deferred() + const entered = deferred() + let nativeSignal: AbortSignal | undefined + let executorClosed = false + let deferredClose: Promise | undefined + let ordinal = 0 + const executor = new BrowserClientPageCommandExecutor({ + ...harness.dependencies, + executeAutomation: async () => ({ title: `small-result-${ordinal++}`, items: [1, 2, 3] }), + routeWebContents: { + ...harness.dependencies.routeWebContents, + navigateGuest: () => { + entered.resolve() + return navigation.promise + } + } + }) + const dispatcher = new BrowserClientHostCommandDispatcher({ + authority, + joinTimeoutMs: 15, + handler: (event, signal) => { + if (event.command.type === 'navigate') { + nativeSignal = signal + } + return executor.handle(event, signal) + } + }) + await dispatcher.dispatch(createCommand('createPage')) + const results: WeakRef[] = [] + for (let sequence = 2; sequence < 34; sequence += 1) { + results.push(await rememberResult(dispatcher, sequence)) + } + await collect() + expect(results.filter((result) => result.deref())).toHaveLength(32) + const pending = dispatcher.dispatch( + command(34, { type: 'navigate', url: 'https://example.invalid/held' }) + ) + await entered.promise + executor.fenceNavigation() + try { + const settled = await closeBrowserClientHostComposition({ + host: { + close: () => dispatcher.close(), + whenHandlersSettled: () => dispatcher.whenClosed() + }, + executor: { + async close() { + executorClosed = true + await executor.close() + } + }, + routeSets: { close: async () => {} }, + error: new Error('controlled disconnect'), + deferExecutorClose: (close) => { + deferredClose = close + }, + reportCleanupError: (error) => { + throw error + } + }) + expect(settled).toBe(false) + await expect(pending).resolves.toMatchObject({ errorCode: 'browser_host_command_cancelled' }) + expect(nativeSignal?.aborted).toBe(true) + expect(executorClosed).toBe(false) + expect(executor.hasPage('page-a', 7)).toBe(true) + expect(harness.route.release).not.toHaveBeenCalled() + expect(harness.routeSession.release).not.toHaveBeenCalled() + expect(() => dispatcher.dispatch(createCommand('createPage'))).toThrow('dispatcher_closed') + expect(await dispatcher.close()).toBe(false) + await collect() + expect(results.filter((result) => result.deref())).toHaveLength(0) + } finally { + navigation.resolve(true) + await dispatcher.whenClosed() + await deferredClose + await executor.close() + } + expect(executorClosed).toBe(true) + expect(harness.route.release).toHaveBeenCalledOnce() + expect(harness.routeSession.release).toHaveBeenCalledOnce() +}) + +it('discards late closed records while retaining a sibling pending handler', async () => { + const first = deferred() + const second = deferred() + const dispatcher = new BrowserClientHostCommandDispatcher({ + authority, + joinTimeoutMs: 15, + handler: (event) => + event.command.type === 'createPage' + ? { status: 'completed' } + : event.browserPageId === 'page-a' + ? first.promise + : second.promise + }) + for (const page of ['page-a', 'page-b']) { + await dispatcher.dispatch( + command( + 1, + { type: 'createPage', browserProfileId: 'profile-a', executionHostKey: 'execution-host-a' }, + page + ) + ) + } + const a = dispatchInput(dispatcher, 'page-a') + const b = dispatchInput(dispatcher, 'page-b') + try { + expect(await dispatcher.close()).toBe(false) + await expect(a.result).resolves.toMatchObject({ errorCode: 'browser_host_command_cancelled' }) + await expect(b.result).resolves.toMatchObject({ errorCode: 'browser_host_command_cancelled' }) + first.resolve({ status: 'completed' }) + await collect() + expect(a.input.deref()).toBeUndefined() + expect(b.input.deref()).toBeDefined() + expect(await dispatcher.close()).toBe(false) + } finally { + first.resolve({ status: 'completed' }) + second.resolve({ status: 'completed' }) + await dispatcher.whenClosed() + } + await collect() + expect(b.input.deref()).toBeUndefined() +}) From 9ed2f743a4dd7aab1d35906c049c11d143a92aaf Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:19:51 -0700 Subject: [PATCH 22/59] fix(runtime): fence terminal snapshot completion by owner (#20996) Co-authored-by: m4air --- .../headless-hydration-retention/README.md | 39 ++++ .../headless-hydration-retention/fix.patch | 158 ++++++++++++++ .../reproduce.mjs | 130 +++++++++++ .../headless-hydration-retention/results.json | 51 +++++ ...adless-hydration-ownership-test-fixture.ts | 82 +++++++ .../headless-hydration-ownership.test.ts | 125 +++++++++++ .../runtime/headless-seed-ownership.test.ts | 202 ++++++++++++++++++ ...untime-capture-provider-terminal-buffer.ts | 11 +- ...time-create-pty-headless-terminal-state.ts | 12 +- ...me-maybe-hydrate-headless-from-renderer.ts | 16 +- ...-runtime-serialize-main-terminal-buffer.ts | 9 + 11 files changed, 827 insertions(+), 8 deletions(-) create mode 100644 docs/audits/headless-hydration-retention/README.md create mode 100644 docs/audits/headless-hydration-retention/fix.patch create mode 100644 docs/audits/headless-hydration-retention/reproduce.mjs create mode 100644 docs/audits/headless-hydration-retention/results.json create mode 100644 src/main/runtime/headless-hydration-ownership-test-fixture.ts create mode 100644 src/main/runtime/headless-hydration-ownership.test.ts create mode 100644 src/main/runtime/headless-seed-ownership.test.ts diff --git a/docs/audits/headless-hydration-retention/README.md b/docs/audits/headless-hydration-retention/README.md new file mode 100644 index 00000000000..7ab6ace96a8 --- /dev/null +++ b/docs/audits/headless-hydration-retention/README.md @@ -0,0 +1,39 @@ +# Late headless snapshot ownership + +## Reproduced defect + +The runtime can retire a PTY or replace its headless model while a renderer/provider snapshot or emulator seed write is pending. The old completion still updates maps keyed only by PTY ID. After exit cleanup, a successful renderer reply recreates CWD, recent-output and title state; even an empty or rejected reply recreates the hydration `done` entry. A replaced model can also lose its provider preference or pending hydration status to the old completion. + +Provider snapshot validation has two related races. Its late generation check calls an allocating getter, recreating an entry that exit just deleted. Its cleanup can delete a newer capture's live-mode scanner Set after the old Set becomes empty. Provider tail parsing has the same allocating late check after an awaited parse/write. + +These paths exist in `v1.4.198`. They explain a concrete main/runtime retention mechanism under PTY churn, but the frequency and retained size in #19831/#19768 remain unproven. The renderer request already has a 750 ms timeout (`src/main/ipc/pty/ipc/serialize-buffer.ts`); this fix addresses callbacks writing after their owner retires, not an indefinite renderer wait. + +## Fix and ownership + +The three model-seeding paths compare `headlessTerminals.get(ptyId)` with the captured state before starting work and after asynchronous boundaries. Completion bookkeeping runs only for that state. Provider capture/tail completion compares the existing generation without allocating, and capture cleanup removes the map entry only while it still owns the same Set. + +Admission generation allocation and normal queued live writes keep their existing behavior. Disposal still drains live writes queued before retirement. Current hydration, query replay suppression, CWD/kitty metadata and replacement capture mode tracking remain covered. These are local runtime ownership checks for both local and SSH-backed terminals; they add no process-death inference, remote cancellation or wire change. They do not depend on git worktrees. + +## Reproduce + +Run from the repository root with installed dependencies: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node docs/audits/headless-hydration-retention/reproduce.mjs +``` + +The script runs the actual `OrcaRuntimeService` regression fixtures twice. For the before case, it reverses only the included four-file `fix.patch` in a temporary Vite transform. It neither rewrites source files nor needs an unpublished commit. The after case uses the checked-out source. Source hashes and individual failing cases are recorded in `results.json`. + +- Before: **16 failed, 6 passed**. +- After: **22 passed**. + +The tests control pending promises to cover retirement before callback admission, during renderer/provider replies, during seed writes and during kitty metadata application. They cover success, null, rejection, same-ID replacement, current-state success, generation preservation and live-mode scanner ownership. Provider-tail checks exercise both normal and visible-screen-only parsing. + +Additional validation: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node node_modules/typescript/bin/tsc --noEmit -p config/tsconfig.node.json +ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-query-responder.test.ts src/main/runtime/headless-hydration-ownership.test.ts src/main/runtime/headless-seed-ownership.test.ts --testNamePattern 'headless|hydrat|WSL|provider cwd|live WSL cwd|query|seed|retire|replacement|capture|renderer' +``` + +Node typecheck passed. The selected existing runtime/query checks plus the new cases passed **306 tests**, with 1087 unrelated cases skipped by the name filter. All runs were headless and used `ORCA_BACKGROUND_LAUNCH=1`. diff --git a/docs/audits/headless-hydration-retention/fix.patch b/docs/audits/headless-hydration-retention/fix.patch new file mode 100644 index 00000000000..dfa283eae32 --- /dev/null +++ b/docs/audits/headless-hydration-retention/fix.patch @@ -0,0 +1,158 @@ +diff --git a/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts b/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts +index acf4f60532..fc9ba31964 100644 +--- a/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts ++++ b/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts +@@ -31,7 +31,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit + // Why: daemon PTYs survive an app relaunch before any renderer mounts. + // Mobile still needs their retained history without navigating desktop. + const snapshot = await this.ptyController?.serializeProviderBuffer?.(ptyId, opts) +- if (!snapshot || this.getPtyLifecycleGeneration(ptyId) !== generation) { ++ if (!snapshot || this.ptyLifecycleGenerationById.get(ptyId) !== generation) { + return null + } + const snapshotModeTracker = new TerminalKittyKeyboardModeTracker() +@@ -73,7 +73,10 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit + return null + } finally { + liveModeTrackers.delete(liveModeTracker) +- if (liveModeTrackers.size === 0) { ++ if ( ++ liveModeTrackers.size === 0 && ++ this.providerModeSnapshotScansByPtyId.get(ptyId) === liveModeTrackers ++ ) { + this.providerModeSnapshotScansByPtyId.delete(ptyId) + } + } +@@ -163,7 +166,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit + if (snapshotOptions.visibleScreenOnly) { + const projection = await this.parseVisibleSnapshot(snapshot) + // Live bytes ordered after the provider frame make that frame stale. +- return this.getPtyLifecycleGeneration(ptyId) === generation && ++ return this.ptyLifecycleGenerationById.get(ptyId) === generation && + this.getPtyOutputSequence(ptyId) <= snapshot.seq + ? projection + : { lines: [] } +@@ -180,7 +183,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit + try { + await emulator.write(data) + const projection = projectTerminalTailLines(emulator, lineLimit) +- return this.getPtyLifecycleGeneration(ptyId) === generation && ++ return this.ptyLifecycleGenerationById.get(ptyId) === generation && + this.getPtyOutputSequence(ptyId) <= snapshot.seq + ? projection + : { lines: [] } +diff --git a/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts b/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts +index da5d824ef6..b9f3547850 100644 +--- a/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts ++++ b/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts +@@ -112,8 +112,11 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi + this.headlessTerminals.set(ptyId, state) + state.writeChain = state.writeChain + .then(async () => { ++ if (this.headlessTerminals.get(ptyId) !== state) { ++ return ++ } + const snapshot = await this.serializeProviderTerminalBuffer(ptyId) +- if (!snapshot) { ++ if (this.headlessTerminals.get(ptyId) !== state || !snapshot) { + return + } + const data = `${snapshot.scrollbackAnsi ?? ''}${snapshot.data}` +@@ -123,6 +126,9 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi + this.recordOsc7MetadataForPty(ptyId, data) + } + await state.emulator.write(data) ++ if (this.headlessTerminals.get(ptyId) !== state) { ++ return ++ } + if (snapshot.cwd !== undefined) { + state.emulator.setCwd(snapshot.cwd) + if (!this.terminalCwdByPtyId.has(ptyId) && snapshot.cwd?.trim()) { +@@ -141,7 +147,9 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi + // Best-effort: live bytes already chain behind this replacement state. + }) + .finally(() => { +- this.providerSnapshotPreferredPtys.delete(ptyId) ++ if (this.headlessTerminals.get(ptyId) === state) { ++ this.providerSnapshotPreferredPtys.delete(ptyId) ++ } + }) + } + +diff --git a/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts b/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts +index 163ffaecdb..63135f261b 100644 +--- a/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts ++++ b/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts +@@ -51,6 +51,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime + // setting headlessTerminals, the live byte would lazy-create a separate + // state and the seed-resolve would overwrite it, dropping live bytes. + state.writeChain = state.writeChain.then(async () => { ++ if (this.headlessTerminals.get(ptyId) !== state) { ++ return ++ } + try { + // Why the scrollback is not suppressed mid-TUI: the seed IS the model's + // normal buffer, so zeroing it while an alt-screen agent was up left the +@@ -58,7 +61,11 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime + const rendered = await controller.serializeBuffer!(ptyId, { + scrollbackRows: MOBILE_SUBSCRIBE_SCROLLBACK_ROWS + }) +- if (!rendered || rendered.data.length === 0) { ++ if ( ++ this.headlessTerminals.get(ptyId) !== state || ++ !rendered || ++ rendered.data.length === 0 ++ ) { + return + } + this.recordOsc7MetadataForPty(ptyId, rendered.data) +@@ -70,6 +77,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime + state.emulator.resize(rendered.cols, rendered.rows) + } + await state.emulator.write(rendered.data) ++ if (this.headlessTerminals.get(ptyId) !== state) { ++ return ++ } + const ptyDims = this.getTerminalSize(ptyId) + if (ptyDims && (ptyDims.cols !== rendered.cols || ptyDims.rows !== rendered.rows)) { + state.emulator.resize(ptyDims.cols, ptyDims.rows) +@@ -91,7 +101,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime + // Hydration is best-effort. Live writes continue via the same + // writeChain that this catch-arm leaves intact. + } finally { +- this.headlessHydrationState.set(ptyId, 'done') ++ if (this.headlessTerminals.get(ptyId) === state) { ++ this.headlessHydrationState.set(ptyId, 'done') ++ } + } + }) + } +diff --git a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts +index 6559dbfd34..5b6da61d14 100644 +--- a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts ++++ b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts +@@ -134,15 +134,24 @@ export class OrcaRuntimeWithSerializeMainTerminalBuffer extends OrcaRuntimeWithA + this.recordRecentPtyOutputForPathProvenance(ptyId, data) + state.writeChain = state.writeChain + .then(async () => { ++ if (this.headlessTerminals.get(ptyId) !== state) { ++ return ++ } + // Why: seed writes never set forwardQueryReplies — the main-side + // replay guard. A snapshot containing old queries must answer no one. + await state.emulator.write(data) ++ if (this.headlessTerminals.get(ptyId) !== state) { ++ return ++ } + // Why AFTER the seed write: the snapshot payload cannot carry kitty + // pushes (rehydrateSequences deliberately omits them), but ordering + // behind it keeps the parse deterministic. Unflagged like the seed — + // re-applying flags must answer no one. + if (typeof metadata.kittyKeyboardFlags === 'number') { + await state.emulator.applyKittyKeyboardFlags(metadata.kittyKeyboardFlags) ++ if (this.headlessTerminals.get(ptyId) !== state) { ++ return ++ } + } + if (metadata.cwd !== undefined) { + state.emulator.setCwd(metadata.cwd) diff --git a/docs/audits/headless-hydration-retention/reproduce.mjs b/docs/audits/headless-hydration-retention/reproduce.mjs new file mode 100644 index 00000000000..dc7bb72bac0 --- /dev/null +++ b/docs/audits/headless-hydration-retention/reproduce.mjs @@ -0,0 +1,130 @@ +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 = {} +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: createHash('sha256').update(before).digest('hex'), + after: createHash('sha256').update(current).digest('hex') + } +} + +const scratch = await mkdtemp(join(tmpdir(), 'orca-hydration-retention-')) +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 baselineConfig = join(scratch, 'before.config.mjs') + const configImport = JSON.stringify(pathToFileURL(resolve(root, 'config/vitest.config.ts')).href) + await writeFile( + baselineConfig, + `import base from ${configImport}; +const beforeSources = ${JSON.stringify(beforeSources)}; +export default {...base, plugins: [{ + name: 'hydrate-before-ownership-fix', enforce: 'pre', + transform(_code, id) { + const before = beforeSources[id.replaceAll('\\\\', '/').split('?')[0]]; + return before === undefined ? null : {code: before, map: null}; + } +}]};\n` + ) + + async function run(label, config) { + const report = join(scratch, `${label}.json`) + const result = await runProcess({ + program: process.execPath, + args: [ + resolve(root, 'node_modules/vitest/vitest.mjs'), + 'run', + '--config', + config, + 'src/main/runtime/headless-hydration-ownership.test.ts', + 'src/main/runtime/headless-seed-ownership.test.ts', + '--reporter=json', + `--outputFile=${report}` + ], + cwd: root, + env: process.env, + timeoutMs: 90_000, + maxOutputBytes: 4 * 1024 * 1024 + }) + let parsed + try { + parsed = JSON.parse(await readFile(report, 'utf8')) + } catch (error) { + throw new Error(`${label} runner failed: ${result.stderr || result.stdout}`, { cause: error }) + } + return { + exitCode: result.code, + passed: parsed.numPassedTests, + failed: parsed.numFailedTests, + failedCases: parsed.testResults.flatMap((suite) => + suite.assertionResults + .filter((test) => test.status === 'failed') + .map((test) => test.fullName) + ) + } + } + + const before = await run('before', baselineConfig) + const after = await run('after', resolve(root, 'config/vitest.config.ts')) + const passed = + before.failed > 0 && + before.passed + before.failed === 22 && + after.passed === 22 && + after.failed === 0 + console.log( + JSON.stringify( + { + comparison: + 'Actual runtime tests; before reverses only fix.patch in a temporary Vite transform', + 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 }) +} diff --git a/docs/audits/headless-hydration-retention/results.json b/docs/audits/headless-hydration-retention/results.json new file mode 100644 index 00000000000..38a45b7cbab --- /dev/null +++ b/docs/audits/headless-hydration-retention/results.json @@ -0,0 +1,51 @@ +{ + "comparison": "Actual runtime tests; before reverses only fix.patch in a temporary Vite transform", + "sourceHashes": { + "src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts": { + "before": "53e8db1da23a2048abe498b4ea9911ad176460322c2cb6da2fcfd5472e6b4b2d", + "after": "9df660b042323ad7e68e093add5b3fbdbfd47f80976710bc657ab16471f07a02" + }, + "src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts": { + "before": "67222b470b133cb473acf85e10de970535cceacf06964a5afeb50d640a0fb0ac", + "after": "21a9aef91c4debaf91aa2bd3a7d7b77bc46769cc7cead0609b8425350ea78636" + }, + "src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts": { + "before": "dd0d060f53512c4f1cddd1f3dbe4cb86d11d2d90fecba41c01f936d2d2344aff", + "after": "5ba68552f81a9dfba1e4f8756732b958a91ecf60b14a62c1c536cca61c5c55e7" + }, + "src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts": { + "before": "c30adce84d951030366b5954a7ad59dcbc902f021f83906d7b58bd90eb7c5a1b", + "after": "c5c533376402d7963360813aebf01299d4e3e950e4945852ddea20d88f3cdbfc" + } + }, + "before": { + "exitCode": 1, + "passed": 6, + "failed": 16, + "failedCases": [ + "does not start renderer hydration after the model retires before its callback", + "does not resurrect retired renderer-hydration state after success", + "does not resurrect retired renderer-hydration state after null", + "does not resurrect retired renderer-hydration state after reject", + "keeps a same-ID replacement pending when an old renderer snapshot arrives", + "skips late title and completion bookkeeping after disposal during the seed write", + "skips an initial seed retired before its callback without clearing the replacement preference", + "keeps replacement ownership when an initial seed awaits write", + "keeps replacement ownership when an initial seed awaits kitty", + "does not acquire a provider snapshot for a model retired before its callback", + "does not retain provider state after a retired acquisition returns success", + "refuses a stale context seed after model replacement within the same PTY generation", + "does not reinsert provider CWD after disposal during its seed write", + "keeps the replacement capture generation and live-mode scan after an old capture settles", + "does not remint a retired generation after parsing a provider tail, visible-only: false", + "does not remint a retired generation after parsing a provider tail, visible-only: true" + ] + }, + "after": { + "exitCode": 0, + "passed": 22, + "failed": 0, + "failedCases": [] + }, + "passed": true +} diff --git a/src/main/runtime/headless-hydration-ownership-test-fixture.ts b/src/main/runtime/headless-hydration-ownership-test-fixture.ts new file mode 100644 index 00000000000..5fb3314419c --- /dev/null +++ b/src/main/runtime/headless-hydration-ownership-test-fixture.ts @@ -0,0 +1,82 @@ +import { afterEach, vi } from 'vitest' +import './orca-runtime-test-lifecycle.spec' +import { OrcaRuntimeService } from './orca-runtime' +import { store, syncSinglePty } from './orca-runtime-test-fixtures.spec' + +export const PTY_ID = 'pty-hydration-owner' +export const SIZE = { cols: 80, rows: 24 } +export const RETIRED_SNAPSHOT = { + data: '\x1b]7;file:///retired-context\x07RETIRED-SEED', + lastTitle: 'Codex working', + ...SIZE +} + +export class HydrationRuntime extends OrcaRuntimeService { + model() { + const state = this.headlessTerminals.get(PTY_ID) + if (!state) { + throw new Error('Expected headless model') + } + return state + } + + retainedState() { + return { + model: this.headlessTerminals.has(PTY_ID), + hydration: this.headlessHydrationState.get(PTY_ID), + cwd: this.terminalCwdByPtyId.get(PTY_ID), + titleTracker: this.ptyTitleTrackersByPtyId.has(PTY_ID), + recentOutput: this.recentPtyOutputById.has(PTY_ID), + providerPreferred: this.providerSnapshotPreferredPtys.has(PTY_ID), + generation: this.ptyLifecycleGenerationById.get(PTY_ID), + snapshotScans: this.providerModeSnapshotScansByPtyId.get(PTY_ID)?.size ?? 0 + } + } + + preferProvider() { + this.providerSnapshotPreferredPtys.add(PTY_ID) + } + + replaceExecutionContext() { + this.replaceHeadlessTerminalAfterExecutionContextChange(PTY_ID) + } + + captureProvider() { + return this.captureProviderTerminalBuffer(PTY_ID, {}, this.getPtyLifecycleGeneration(PTY_ID)) + } + + providerTail(visibleScreenOnly: boolean) { + return this.readProviderTerminalTailLines(PTY_ID, 10, { visibleScreenOnly }) + } +} + +const runtimes: HydrationRuntime[] = [] + +export function createHydrationRuntime(): HydrationRuntime { + const runtime = new HydrationRuntime(store) + syncSinglePty(runtime, PTY_ID) + runtimes.push(runtime) + return runtime +} + +export function retire(runtime: HydrationRuntime): void { + runtime.onPtyExit(PTY_ID, 0, undefined, { providerExitObserved: true }) +} + +export const EMPTY_RETAINED_STATE = { + model: false, + hydration: undefined, + cwd: undefined, + titleTracker: false, + recentOutput: false, + providerPreferred: false, + generation: undefined, + snapshotScans: 0 +} + +afterEach(() => { + for (const runtime of runtimes.splice(0)) { + retire(runtime) + } + vi.restoreAllMocks() +}) diff --git a/src/main/runtime/headless-hydration-ownership.test.ts b/src/main/runtime/headless-hydration-ownership.test.ts new file mode 100644 index 00000000000..675a57bf70b --- /dev/null +++ b/src/main/runtime/headless-hydration-ownership.test.ts @@ -0,0 +1,125 @@ +import { expect, it, vi } from 'vitest' +import { deferred, makeDeferred } from './orca-runtime-test-fixtures.spec' +import { + createHydrationRuntime, + EMPTY_RETAINED_STATE, + PTY_ID, + RETIRED_SNAPSHOT, + retire, + SIZE +} from './headless-hydration-ownership-test-fixture' + +type Snapshot = typeof RETIRED_SNAPSHOT | null + +function prepare() { + const runtime = createHydrationRuntime() + const snapshot = deferred() + const serialize = vi.fn(() => snapshot.promise) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + getSize: () => SIZE, + hasRendererSerializer: () => true, + serializeBuffer: serialize + }) + return { runtime, snapshot, serialize } +} + +it('does not start renderer hydration after the model retires before its callback', async () => { + const { runtime, snapshot, serialize } = prepare() + runtime.onPtyData(PTY_ID, 'queued-live', 1) + const old = runtime.model() + const write = vi.spyOn(old.emulator, 'write') + retire(runtime) + snapshot.resolve(RETIRED_SNAPSHOT) + await old.writeChain + expect(serialize).not.toHaveBeenCalled() + expect(write).toHaveBeenCalledWith('queued-live', { forwardQueryReplies: false }) + expect(runtime.retainedState()).toEqual(EMPTY_RETAINED_STATE) +}) + +it.each(['success', 'null', 'reject'] as const)( + 'does not resurrect retired renderer-hydration state after %s', + async (outcome) => { + const { runtime, snapshot, serialize } = prepare() + runtime.onPtyData(PTY_ID, 'queued-live', 1) + const old = runtime.model() + await vi.waitFor(() => expect(serialize).toHaveBeenCalledOnce()) + retire(runtime) + if (outcome === 'reject') { + snapshot.reject(new Error('Renderer unavailable')) + } else { + snapshot.resolve(outcome === 'success' ? RETIRED_SNAPSHOT : null) + } + await old.writeChain + expect(runtime.retainedState()).toEqual(EMPTY_RETAINED_STATE) + } +) + +it.each(['success', 'null', 'reject'] as const)( + 'settles current renderer hydration after %s and preserves queued live bytes', + async (outcome) => { + const { runtime, snapshot } = prepare() + runtime.onPtyData(PTY_ID, 'CURRENT-LIVE', 1) + const current = runtime.model() + if (outcome === 'reject') { + snapshot.reject(new Error('Renderer unavailable')) + } else { + snapshot.resolve(outcome === 'success' ? RETIRED_SNAPSHOT : null) + } + await current.writeChain + expect(runtime.retainedState().hydration).toBe('done') + expect(current.emulator.getVisibleLines().join('\n')).toContain('CURRENT-LIVE') + expect(current.emulator.getVisibleLines().join('\n').includes('RETIRED-SEED')).toBe( + outcome === 'success' + ) + } +) + +it('keeps a same-ID replacement pending when an old renderer snapshot arrives', async () => { + const { runtime, snapshot, serialize } = prepare() + runtime.onPtyData(PTY_ID, 'OLD-LIVE', 1) + const old = runtime.model() + await vi.waitFor(() => expect(serialize).toHaveBeenCalledOnce()) + runtime.notePtyDataGap(PTY_ID) + const replacementSnapshot = deferred() + serialize.mockImplementation(() => replacementSnapshot.promise) + runtime.onPtyData(PTY_ID, 'NEW-LIVE', 2) + const replacement = runtime.model() + runtime.preferProvider() + await vi.waitFor(() => expect(serialize).toHaveBeenCalledTimes(2)) + snapshot.resolve(RETIRED_SNAPSHOT) + await old.writeChain + expect(runtime.model()).toBe(replacement) + expect(runtime.retainedState()).toMatchObject({ hydration: 'pending', providerPreferred: true }) + expect(runtime.retainedState().cwd).toBeUndefined() + replacementSnapshot.resolve({ ...RETIRED_SNAPSHOT, data: 'NEW-SEED', lastTitle: 'New title' }) + await replacement.writeChain + const text = replacement.emulator.getVisibleLines().join('\n') + expect(text).toContain('NEW-SEEDNEW-LIVE') + expect(text).not.toContain('OLD-LIVE') + expect(text).not.toContain('RETIRED-SEED') + expect(runtime.retainedState()).toMatchObject({ hydration: 'done', providerPreferred: false }) +}) + +it('skips late title and completion bookkeeping after disposal during the seed write', async () => { + const { runtime, snapshot, serialize } = prepare() + runtime.onPtyData(PTY_ID, 'queued-live', 1) + const old = runtime.model() + const started = makeDeferred() + const release = makeDeferred() + const original = old.emulator.write.bind(old.emulator) + vi.spyOn(old.emulator, 'write').mockImplementationOnce(async (data) => { + started.resolve() + await release.promise + return original(data) + }) + await vi.waitFor(() => expect(serialize).toHaveBeenCalledOnce()) + snapshot.resolve(RETIRED_SNAPSHOT) + await started.promise + retire(runtime) + release.resolve() + await old.writeChain + expect(runtime.retainedState()).toEqual(EMPTY_RETAINED_STATE) +}) diff --git a/src/main/runtime/headless-seed-ownership.test.ts b/src/main/runtime/headless-seed-ownership.test.ts new file mode 100644 index 00000000000..39963e531d2 --- /dev/null +++ b/src/main/runtime/headless-seed-ownership.test.ts @@ -0,0 +1,202 @@ +import { expect, it, vi } from 'vitest' +import { HeadlessEmulator } from '../daemon/headless-emulator' +import { deferred, makeDeferred, syncSinglePty } from './orca-runtime-test-fixtures.spec' +import type { PtyProviderBufferSnapshot } from '../providers/types' +import { + createHydrationRuntime, + EMPTY_RETAINED_STATE, + PTY_ID, + retire, + SIZE +} from './headless-hydration-ownership-test-fixture' + +const PROVIDER_SNAPSHOT: PtyProviderBufferSnapshot = { + ...SIZE, + data: 'PROVIDER-SEED', + cwd: '/retired-context', + seq: 0, + source: 'headless', + alternateScreen: false +} + +function prepareProvider() { + const runtime = createHydrationRuntime() + const snapshot = deferred() + const serialize = vi.fn(() => snapshot.promise) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + getSize: () => SIZE, + serializeProviderBuffer: serialize + }) + return { runtime, snapshot, serialize } +} + +it('skips an initial seed retired before its callback without clearing the replacement preference', async () => { + const runtime = createHydrationRuntime() + runtime.seedHeadlessTerminal(PTY_ID, 'OLD-SEED') + const old = runtime.model() + const write = vi.spyOn(old.emulator, 'write') + runtime.notePtyDataGap(PTY_ID) + runtime.onPtyData(PTY_ID, 'NEW-LIVE', 1) + const replacement = runtime.model() + runtime.preferProvider() + await old.writeChain + await replacement.writeChain + expect(write).not.toHaveBeenCalled() + expect(runtime.retainedState().providerPreferred).toBe(true) + expect(replacement.emulator.getVisibleLines().join('\n')).toContain('NEW-LIVE') +}) + +it.each(['write', 'kitty'] as const)( + 'keeps replacement ownership when an initial seed awaits %s', + async (stage) => { + const runtime = createHydrationRuntime() + runtime.seedHeadlessTerminal(PTY_ID, 'OLD-SEED', SIZE, { kittyKeyboardFlags: 3 }) + const old = runtime.model() + const started = makeDeferred() + const release = makeDeferred() + if (stage === 'write') { + const original = old.emulator.write.bind(old.emulator) + vi.spyOn(old.emulator, 'write').mockImplementationOnce(async (data) => { + started.resolve() + await release.promise + return original(data) + }) + } else { + const original = old.emulator.applyKittyKeyboardFlags.bind(old.emulator) + vi.spyOn(old.emulator, 'applyKittyKeyboardFlags').mockImplementationOnce(async (flags) => { + started.resolve() + await release.promise + return original(flags) + }) + } + await started.promise + runtime.notePtyDataGap(PTY_ID) + runtime.onPtyData(PTY_ID, 'NEW-LIVE', 1) + runtime.preferProvider() + release.resolve() + await old.writeChain + expect(runtime.retainedState().providerPreferred).toBe(true) + } +) + +it('preserves current seed metadata and ordered live output', async () => { + const runtime = createHydrationRuntime() + runtime.seedHeadlessTerminal(PTY_ID, 'SEED-', SIZE, { cwd: '/current', kittyKeyboardFlags: 3 }) + runtime.onPtyData(PTY_ID, 'LIVE', 1) + await runtime.model().writeChain + const snapshot = runtime.model().emulator.getSnapshot() + expect(snapshot.snapshotAnsi).toContain('SEED-LIVE') + expect(snapshot.cwd).toBe('/current') + expect(snapshot.modes.kittyKeyboardFlags).toBe(3) +}) + +it('does not acquire a provider snapshot for a model retired before its callback', async () => { + const { runtime, snapshot, serialize } = prepareProvider() + runtime.replaceExecutionContext() + const old = runtime.model() + retire(runtime) + snapshot.resolve(PROVIDER_SNAPSHOT) + await old.writeChain + expect(serialize).not.toHaveBeenCalled() + expect(runtime.retainedState()).toEqual(EMPTY_RETAINED_STATE) +}) + +it.each(['success', 'null', 'reject'] as const)( + 'does not retain provider state after a retired acquisition returns %s', + async (outcome) => { + const { runtime, snapshot, serialize } = prepareProvider() + runtime.replaceExecutionContext() + const old = runtime.model() + await vi.waitFor(() => expect(serialize).toHaveBeenCalledOnce()) + retire(runtime) + if (outcome === 'reject') { + snapshot.reject(new Error('Provider unavailable')) + } else { + snapshot.resolve(outcome === 'success' ? PROVIDER_SNAPSHOT : null) + } + await old.writeChain + expect(runtime.retainedState()).toEqual(EMPTY_RETAINED_STATE) + } +) + +it('refuses a stale context seed after model replacement within the same PTY generation', async () => { + const { runtime, snapshot, serialize } = prepareProvider() + runtime.replaceExecutionContext() + const old = runtime.model() + await vi.waitFor(() => expect(serialize).toHaveBeenCalledOnce()) + runtime.notePtyDataGap(PTY_ID) + runtime.onPtyData(PTY_ID, 'NEW-LIVE', 1) + const replacement = runtime.model() + runtime.preferProvider() + snapshot.resolve(PROVIDER_SNAPSHOT) + await old.writeChain + expect(runtime.model()).toBe(replacement) + expect(runtime.retainedState()).toMatchObject({ cwd: undefined, providerPreferred: true }) +}) + +it('does not reinsert provider CWD after disposal during its seed write', async () => { + const { runtime, snapshot } = prepareProvider() + runtime.replaceExecutionContext() + const old = runtime.model() + const started = makeDeferred() + const release = makeDeferred() + const original = old.emulator.write.bind(old.emulator) + vi.spyOn(old.emulator, 'write').mockImplementationOnce(async (data) => { + started.resolve() + await release.promise + return original(data) + }) + snapshot.resolve(PROVIDER_SNAPSHOT) + await started.promise + retire(runtime) + release.resolve() + await old.writeChain + expect(runtime.retainedState()).toEqual(EMPTY_RETAINED_STATE) +}) + +it('keeps the replacement capture generation and live-mode scan after an old capture settles', async () => { + const { runtime, snapshot, serialize } = prepareProvider() + const old = runtime.captureProvider() + const oldGeneration = runtime.retainedState().generation + retire(runtime) + syncSinglePty(runtime, PTY_ID) + const replacementSnapshot = deferred() + serialize.mockImplementation(() => replacementSnapshot.promise) + const replacement = runtime.captureProvider() + const newGeneration = runtime.retainedState().generation + expect(newGeneration).not.toBe(oldGeneration) + snapshot.resolve(PROVIDER_SNAPSHOT) + await expect(old).resolves.toBeNull() + expect(runtime.retainedState()).toMatchObject({ generation: newGeneration, snapshotScans: 1 }) + runtime.onPtyData(PTY_ID, '\x1b[?1049h', 1) + replacementSnapshot.resolve(PROVIDER_SNAPSHOT) + await expect(replacement).resolves.toMatchObject({ alternateScreen: true }) + expect(runtime.retainedState().snapshotScans).toBe(0) +}) + +it.each([false, true])( + 'does not remint a retired generation after parsing a provider tail, visible-only: %s', + async (visibleOnly) => { + const { runtime, snapshot } = prepareProvider() + const started = makeDeferred() + const release = makeDeferred() + const original = HeadlessEmulator.prototype.write + vi.spyOn(HeadlessEmulator.prototype, 'write').mockImplementationOnce( + async function (this: HeadlessEmulator, data, options) { + started.resolve() + await release.promise + return original.call(this, data, options) + } + ) + const read = runtime.providerTail(visibleOnly) + snapshot.resolve(PROVIDER_SNAPSHOT) + await started.promise + retire(runtime) + release.resolve() + await expect(read).resolves.toEqual({ lines: [] }) + expect(runtime.retainedState()).toEqual(EMPTY_RETAINED_STATE) + } +) diff --git a/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts b/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts index acf4f60532a..fc9ba31964a 100644 --- a/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts +++ b/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts @@ -31,7 +31,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit // Why: daemon PTYs survive an app relaunch before any renderer mounts. // Mobile still needs their retained history without navigating desktop. const snapshot = await this.ptyController?.serializeProviderBuffer?.(ptyId, opts) - if (!snapshot || this.getPtyLifecycleGeneration(ptyId) !== generation) { + if (!snapshot || this.ptyLifecycleGenerationById.get(ptyId) !== generation) { return null } const snapshotModeTracker = new TerminalKittyKeyboardModeTracker() @@ -73,7 +73,10 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit return null } finally { liveModeTrackers.delete(liveModeTracker) - if (liveModeTrackers.size === 0) { + if ( + liveModeTrackers.size === 0 && + this.providerModeSnapshotScansByPtyId.get(ptyId) === liveModeTrackers + ) { this.providerModeSnapshotScansByPtyId.delete(ptyId) } } @@ -163,7 +166,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit if (snapshotOptions.visibleScreenOnly) { const projection = await this.parseVisibleSnapshot(snapshot) // Live bytes ordered after the provider frame make that frame stale. - return this.getPtyLifecycleGeneration(ptyId) === generation && + return this.ptyLifecycleGenerationById.get(ptyId) === generation && this.getPtyOutputSequence(ptyId) <= snapshot.seq ? projection : { lines: [] } @@ -180,7 +183,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit try { await emulator.write(data) const projection = projectTerminalTailLines(emulator, lineLimit) - return this.getPtyLifecycleGeneration(ptyId) === generation && + return this.ptyLifecycleGenerationById.get(ptyId) === generation && this.getPtyOutputSequence(ptyId) <= snapshot.seq ? projection : { lines: [] } diff --git a/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts b/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts index da5d824ef62..b9f35478505 100644 --- a/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts +++ b/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts @@ -112,8 +112,11 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi this.headlessTerminals.set(ptyId, state) state.writeChain = state.writeChain .then(async () => { + if (this.headlessTerminals.get(ptyId) !== state) { + return + } const snapshot = await this.serializeProviderTerminalBuffer(ptyId) - if (!snapshot) { + if (this.headlessTerminals.get(ptyId) !== state || !snapshot) { return } const data = `${snapshot.scrollbackAnsi ?? ''}${snapshot.data}` @@ -123,6 +126,9 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi this.recordOsc7MetadataForPty(ptyId, data) } await state.emulator.write(data) + if (this.headlessTerminals.get(ptyId) !== state) { + return + } if (snapshot.cwd !== undefined) { state.emulator.setCwd(snapshot.cwd) if (!this.terminalCwdByPtyId.has(ptyId) && snapshot.cwd?.trim()) { @@ -141,7 +147,9 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi // Best-effort: live bytes already chain behind this replacement state. }) .finally(() => { - this.providerSnapshotPreferredPtys.delete(ptyId) + if (this.headlessTerminals.get(ptyId) === state) { + this.providerSnapshotPreferredPtys.delete(ptyId) + } }) } diff --git a/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts b/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts index 163ffaecdb3..63135f261b7 100644 --- a/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts +++ b/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts @@ -51,6 +51,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime // setting headlessTerminals, the live byte would lazy-create a separate // state and the seed-resolve would overwrite it, dropping live bytes. state.writeChain = state.writeChain.then(async () => { + if (this.headlessTerminals.get(ptyId) !== state) { + return + } try { // Why the scrollback is not suppressed mid-TUI: the seed IS the model's // normal buffer, so zeroing it while an alt-screen agent was up left the @@ -58,7 +61,11 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime const rendered = await controller.serializeBuffer!(ptyId, { scrollbackRows: MOBILE_SUBSCRIBE_SCROLLBACK_ROWS }) - if (!rendered || rendered.data.length === 0) { + if ( + this.headlessTerminals.get(ptyId) !== state || + !rendered || + rendered.data.length === 0 + ) { return } this.recordOsc7MetadataForPty(ptyId, rendered.data) @@ -70,6 +77,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime state.emulator.resize(rendered.cols, rendered.rows) } await state.emulator.write(rendered.data) + if (this.headlessTerminals.get(ptyId) !== state) { + return + } const ptyDims = this.getTerminalSize(ptyId) if (ptyDims && (ptyDims.cols !== rendered.cols || ptyDims.rows !== rendered.rows)) { state.emulator.resize(ptyDims.cols, ptyDims.rows) @@ -91,7 +101,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime // Hydration is best-effort. Live writes continue via the same // writeChain that this catch-arm leaves intact. } finally { - this.headlessHydrationState.set(ptyId, 'done') + if (this.headlessTerminals.get(ptyId) === state) { + this.headlessHydrationState.set(ptyId, 'done') + } } }) } diff --git a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts index 6559dbfd349..5b6da61d149 100644 --- a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts +++ b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts @@ -134,15 +134,24 @@ export class OrcaRuntimeWithSerializeMainTerminalBuffer extends OrcaRuntimeWithA this.recordRecentPtyOutputForPathProvenance(ptyId, data) state.writeChain = state.writeChain .then(async () => { + if (this.headlessTerminals.get(ptyId) !== state) { + return + } // Why: seed writes never set forwardQueryReplies — the main-side // replay guard. A snapshot containing old queries must answer no one. await state.emulator.write(data) + if (this.headlessTerminals.get(ptyId) !== state) { + return + } // Why AFTER the seed write: the snapshot payload cannot carry kitty // pushes (rehydrateSequences deliberately omits them), but ordering // behind it keeps the parse deterministic. Unflagged like the seed — // re-applying flags must answer no one. if (typeof metadata.kittyKeyboardFlags === 'number') { await state.emulator.applyKittyKeyboardFlags(metadata.kittyKeyboardFlags) + if (this.headlessTerminals.get(ptyId) !== state) { + return + } } if (metadata.cwd !== undefined) { state.emulator.setCwd(metadata.cwd) From 54500a4281f97e434940dc4a27ce5352b6796641 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:21:45 -0700 Subject: [PATCH 23/59] Release hang watchdog quit listener on shutdown (#20910) Co-authored-by: m4air Co-authored-by: m4air --- src/main/hang-watchdog/main-thread-hang-watchdog.test.ts | 6 +++++- src/main/hang-watchdog/main-thread-hang-watchdog.ts | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/hang-watchdog/main-thread-hang-watchdog.test.ts b/src/main/hang-watchdog/main-thread-hang-watchdog.test.ts index eaaea54225b..cd3811b19c4 100644 --- a/src/main/hang-watchdog/main-thread-hang-watchdog.test.ts +++ b/src/main/hang-watchdog/main-thread-hang-watchdog.test.ts @@ -10,7 +10,8 @@ const { workerState, appMock } = vi.hoisted(() => ({ appMock: { isPackaged: true, getAppPath: vi.fn(() => '/apps/orca/app.asar'), - on: vi.fn() + on: vi.fn(), + off: vi.fn() } })) @@ -58,6 +59,7 @@ describe('installMainThreadHangWatchdog', () => { workerState.instance = null workerState.error = null appMock.on.mockReset() + appMock.off.mockReset() appMock.isPackaged = true delete process.env.ORCA_HANG_WATCHDOG_FORCE delete process.env.ORCA_HANG_WATCHDOG_TIMEOUT_MS @@ -137,6 +139,7 @@ describe('installMainThreadHangWatchdog', () => { handle?.stop() expect(worker.postMessage.mock.calls.some(([m]) => m.type === 'shutdown')).toBe(true) + expect(appMock.off).toHaveBeenCalledWith('will-quit', expect.any(Function)) handle?.stop() const shutdowns = worker.postMessage.mock.calls.filter(([m]) => m.type === 'shutdown') @@ -173,6 +176,7 @@ describe('installMainThreadHangWatchdog', () => { const exitListener = worker.once.mock.calls.find(([event]) => event === 'exit')?.[1] expect(exitListener).toEqual(expect.any(Function)) exitListener() + expect(appMock.off).toHaveBeenCalledWith('will-quit', expect.any(Function)) vi.advanceTimersByTime(6_000) expect(worker.postMessage).not.toHaveBeenCalled() }) diff --git a/src/main/hang-watchdog/main-thread-hang-watchdog.ts b/src/main/hang-watchdog/main-thread-hang-watchdog.ts index 46454429fc7..5e6155b6be7 100644 --- a/src/main/hang-watchdog/main-thread-hang-watchdog.ts +++ b/src/main/hang-watchdog/main-thread-hang-watchdog.ts @@ -73,11 +73,15 @@ export function installMainThreadHangWatchdog(options: { return } stopped = true + // Drop the app-level callback as soon as this watchdog is retired so a + // closed worker cannot keep its closure (and worker handle) alive. + app.off('will-quit', stop) clearInterval(heartbeatTimer) postMessage({ type: 'shutdown' }) } worker.once('exit', () => { stopped = true + app.off('will-quit', stop) clearInterval(heartbeatTimer) }) worker.unref() From a0371806303c78755ede4461510098eabde0490b Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:25:19 -0700 Subject: [PATCH 24/59] fix(ai-vault): release retired search write fences (#20986) * fix(ai-vault): release retired search write fences * test(ai-vault): use checked search writer mocks * test: use typed access in memory retention regressions --------- Co-authored-by: m4air Co-authored-by: m4air --- .../session-search-write-fences/README.md | 49 ++++ .../session-search-write-fences/reproduce.mjs | 108 +++++++++ .../session-search-write-fences/results.json | 23 ++ .../session-search-index-consumer.ts | 3 + .../session-search-index-writer.test.ts | 40 ++-- .../session-search-index-writer.ts | 65 ++++-- .../ai-vault-search/session-search-store.ts | 1 + .../session-search-write-lifetime.test.ts | 209 ++++++++++++++++++ 8 files changed, 460 insertions(+), 38 deletions(-) create mode 100644 docs/audits/session-search-write-fences/README.md create mode 100644 docs/audits/session-search-write-fences/reproduce.mjs create mode 100644 docs/audits/session-search-write-fences/results.json create mode 100644 src/main/ai-vault-search/session-search-write-lifetime.test.ts diff --git a/docs/audits/session-search-write-fences/README.md b/docs/audits/session-search-write-fences/README.md new file mode 100644 index 00000000000..c5637e732b1 --- /dev/null +++ b/docs/audits/session-search-write-fences/README.md @@ -0,0 +1,49 @@ +# Session-search write fence retention + +The search writer remembered every removed path for its lifetime. Those counters +fenced a read whose source disappeared before its first commit: both the original +and deleted database cursors are absent, so comparing cursors alone cannot detect +the removal. Counters for paths with no remaining reads were never released. + +The fix tracks only active reads. Removal marks their captured lifetime as removed +and releases the path from the map immediately. New reads get a fresh lifetime; +cleanup from an older read cannot delete it. Final commit and explicit discard +release ownership, while intermediate chunk commits keep it. Consumer errors, +incomplete reads, throwing error reporters, and store close release their fences. + +## Reproduce + +From the repository root with dependencies installed and a Node version providing +`node:sqlite`: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --max-old-space-size=128 docs/audits/session-search-write-fences/reproduce.mjs +``` + +The script bundles the actual writer twice, using the production SQLite schema and +adapter. The baseline is published commit +`243f4431557471daa05636aed1a30be790485eda` (#20551), whose writer matches the pre-fix +source. The after version is the working tree. Only this named Git object is read; +the script fetches nothing. Results include both source hashes and runtime details. + +After 1,000 complete index/retire cycles: + +| Source | Remaining file rows | Retained path entries | Never-indexed stale commit accepted | +| ------ | ------------------: | --------------------: | ----------------------------------- | +| Before | 0 | 1,000 | No | +| After | 0 | 0 | No | + +The regression suite additionally drives the actual consumer/channel path, checks +intermediate flush ownership, failed/incomplete reads, throwing error reporters, +new-generation protection, idempotent discard, and close: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/ai-vault-search/session-search-write-lifetime.test.ts src/main/ai-vault-search/session-search-file-write.test.ts src/main/ai-vault-search/session-search-index-writer.test.ts src/main/ai-vault-search/session-search-index-consumer.test.ts +``` + +Existing retry bookkeeping can recreate a failed `files` metadata row when a +removed read finishes. Its session/messages stay absent and it cannot restore +searchable content. This patch preserves that status policy. + +This is current-code path metadata in the scanner child. The search writer did not +exist in `v1.4.198`; this finding does not explain the reported #19831/#19768 build. diff --git a/docs/audits/session-search-write-fences/reproduce.mjs b/docs/audits/session-search-write-fences/reproduce.mjs new file mode 100644 index 00000000000..531e93b2854 --- /dev/null +++ b/docs/audits/session-search-write-fences/reproduce.mjs @@ -0,0 +1,108 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import Module from 'node:module' +import { join } from 'node:path' +import { fileURLToPath } 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 sourcePath = 'src/main/ai-vault-search/session-search-index-writer.ts' +const baseline = '243f4431557471daa05636aed1a30be790485eda' +const before = execFileSync('git', ['show', `${baseline}:${sourcePath}`], { + cwd: root, + encoding: 'utf8', + maxBuffer: 1024 * 1024 +}) +const after = readFileSync(join(root, sourcePath), 'utf8') + +async function run(version, source) { + const built = await build({ + absWorkingDir: root, + stdin: { + contents: `export { SessionSearchIndexWriter } from './${sourcePath}'; +export { openSessionSearchDatabase } from './src/main/ai-vault-search/session-search-schema.ts';`, + resolveDir: root + }, + bundle: true, + platform: 'node', + format: 'cjs', + packages: 'external', + write: false, + plugins: [ + { + name: 'select-writer-version', + setup(bundler) { + bundler.onLoad({ filter: /session-search-index-writer\.ts$/ }, () => ({ + contents: source, + loader: 'ts' + })) + } + } + ] + }) + const compiled = new Module(join(root, 'session-search-write-fence-probe.cjs')) + compiled.filename = join(root, 'session-search-write-fence-probe.cjs') + compiled.paths = Module._nodeModulePaths(root) + compiled._compile(built.outputFiles[0].text, compiled.filename) + const db = compiled.exports.openSessionSearchDatabase(':memory:') + const writer = new compiled.exports.SessionSearchIndexWriter(db) + const candidate = (path) => ({ + agent: 'claude', + codexHome: null, + file: { path, mtimeMs: 1, modifiedAt: new Date(1).toISOString(), sizeBytes: 1 } + }) + const outcome = { session: null, byteOffset: 1, incomplete: false } + const tracked = version === 'before' ? writer.removals : writer.activeWrites + assert.ok(tracked instanceof Map) + try { + for (let index = 0; index < 1000; index++) { + const path = join('synthetic', `retired-${index}.jsonl`) + const write = writer.beginWrite(candidate(path), 'replace', 0) + assert.equal(write.commit(outcome), true) + writer.removeFile(path) + } + const retainedPathsAfterRetirement = tracked.size + const remainingFiles = db.prepare('SELECT count(*) AS count FROM files').get().count + assert.equal(retainedPathsAfterRetirement, version === 'before' ? 1000 : 0) + assert.equal(remainingFiles, 0) + const removed = join('synthetic', 'never-indexed.jsonl') + const stale = writer.beginWrite(candidate(removed), 'replace', 0) + writer.removeFile(removed) + const staleCommitAccepted = stale.commit(outcome) + assert.equal(staleCommitAccepted, false) + assert.equal(db.prepare('SELECT count(*) AS count FROM files').get().count, 0) + return { + source: version === 'before' ? baseline : 'working tree', + sourceSha256: createHash('sha256').update(source).digest('hex'), + retiredFiles: 1000, + remainingFiles, + retainedPathsAfterRetirement, + neverIndexedStaleCommitAccepted: staleCommitAccepted + } + } finally { + writer.close?.() + db.close() + } +} + +console.log( + JSON.stringify( + { + node: process.version, + platform: process.platform, + architecture: process.arch, + sourcePath, + database: 'Production schema and adapter with an in-memory SQLite database', + before: await run('before', before), + after: await run('after', after) + }, + null, + 2 + ) +) diff --git a/docs/audits/session-search-write-fences/results.json b/docs/audits/session-search-write-fences/results.json new file mode 100644 index 00000000000..471e56fc419 --- /dev/null +++ b/docs/audits/session-search-write-fences/results.json @@ -0,0 +1,23 @@ +{ + "node": "v26.6.0", + "platform": "darwin", + "architecture": "arm64", + "sourcePath": "src/main/ai-vault-search/session-search-index-writer.ts", + "database": "Production schema and adapter with an in-memory SQLite database", + "before": { + "source": "243f4431557471daa05636aed1a30be790485eda", + "sourceSha256": "cd636c04251ab98038d696acc8c75a330f79b413657a1dba57a766bd1356017d", + "retiredFiles": 1000, + "remainingFiles": 0, + "retainedPathsAfterRetirement": 1000, + "neverIndexedStaleCommitAccepted": false + }, + "after": { + "source": "working tree", + "sourceSha256": "a130b93059cf6d19814b998c73b56615058eae08c9a84b148cd819d06a04bc1b", + "retiredFiles": 1000, + "remainingFiles": 0, + "retainedPathsAfterRetirement": 0, + "neverIndexedStaleCommitAccepted": false + } +} diff --git a/src/main/ai-vault-search/session-search-index-consumer.ts b/src/main/ai-vault-search/session-search-index-consumer.ts index a2c0b3b8836..1bad60a9eb2 100644 --- a/src/main/ai-vault-search/session-search-index-consumer.ts +++ b/src/main/ai-vault-search/session-search-index-consumer.ts @@ -77,6 +77,7 @@ class SessionSearchReadConsumer implements TranscriptReadConsumer { // keeps the whole read on one path — the buffer is dropped and the file is // re-read. this.failed = true + this.write.discard() this.store.reportWriteFailure(error) } } @@ -90,6 +91,8 @@ class SessionSearchReadConsumer implements TranscriptReadConsumer { committed = !this.failed && !outcome.incomplete && this.write.commit(outcome) } catch (error) { this.store.reportWriteFailure(error) + } finally { + this.write.discard() } if (committed) { this.store.writeCommitted(candidate) diff --git a/src/main/ai-vault-search/session-search-index-writer.test.ts b/src/main/ai-vault-search/session-search-index-writer.test.ts index 1be12e35bc7..b53393e0cd6 100644 --- a/src/main/ai-vault-search/session-search-index-writer.test.ts +++ b/src/main/ai-vault-search/session-search-index-writer.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, it } from 'vitest' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { SessionSearchIndexConsumer } from './session-search-index-consumer' import { openSessionSearchIndexFile, @@ -25,6 +25,7 @@ beforeEach(async () => { }) afterEach(async () => { + vi.restoreAllMocks() store.close() await index.close() }) @@ -111,15 +112,13 @@ it('refuses to commit a write whose file was removed mid-read', () => { it('declines a behind cursor in beginRead before it ever reaches the store', () => { const attempted: number[] = [] - const stub = { - indexedFile: () => ({ byteOffset: 100, mtimeMs: 1, sizeBytes: 1 }), - beginWrite: (_candidate: unknown, _mode: unknown, previousByteOffset: number) => { - attempted.push(previousByteOffset) - return { add: () => undefined, commit: () => true } - }, - setFileState: () => undefined - } as unknown as SessionSearchStore - const consumer = new SessionSearchIndexConsumer(stub) + vi.spyOn(store, 'indexedFile').mockReturnValue({ byteOffset: 100, mtimeMs: 1, sizeBytes: 1 }) + vi.spyOn(store, 'beginWrite').mockImplementation((_candidate, _mode, previousByteOffset) => { + attempted.push(previousByteOffset) + return { add: () => undefined, commit: () => true, discard: () => undefined } + }) + vi.spyOn(store, 'setFileState').mockImplementation(() => undefined) + const consumer = new SessionSearchIndexConsumer(store) expect( consumer.beginRead({ @@ -142,22 +141,17 @@ it('declines a behind cursor in beginRead before it ever reaches the store', () it("hands the read's identity accessor to the store", () => { const captured: unknown[] = [] - const stub = { - indexedFile: () => null, - beginWrite: ( - _candidate: unknown, - _mode: unknown, - _previousByteOffset: unknown, - identity: unknown - ) => { + vi.spyOn(store, 'indexedFile').mockReturnValue(null) + vi.spyOn(store, 'beginWrite').mockImplementation( + (_candidate, _mode, _previousByteOffset, identity) => { captured.push(identity) - return { add: () => undefined, commit: () => true } - }, - setFileState: () => undefined - } as unknown as SessionSearchStore + return { add: () => undefined, commit: () => true, discard: () => undefined } + } + ) + vi.spyOn(store, 'setFileState').mockImplementation(() => undefined) const identity = (): null => null - new SessionSearchIndexConsumer(stub).beginRead({ + new SessionSearchIndexConsumer(store).beginRead({ candidate: syntheticCandidate(), mode: 'replace', previousByteOffset: 0, diff --git a/src/main/ai-vault-search/session-search-index-writer.ts b/src/main/ai-vault-search/session-search-index-writer.ts index 5e29f2004af..82c9e679af6 100644 --- a/src/main/ai-vault-search/session-search-index-writer.ts +++ b/src/main/ai-vault-search/session-search-index-writer.ts @@ -71,20 +71,20 @@ export type SessionSearchFileWrite = { */ add(message: TranscriptMessage): void /** - * Writes this file's rows, its session and its cursor in one transaction. + * Finishes this read, writing its rows, session and cursor in one transaction. * False when the file's record changed under this read — it was removed, or * another writer moved the cursor these rows continue from. A read that never * calls this leaves the index exactly as it found it, unless it chunked. */ commit(outcome: TranscriptReadOutcome): boolean + /** Ends an incomplete or failed read without publishing its buffered rows. */ + discard(): void } export class SessionSearchIndexWriter { private readonly records: SessionSearchFileRecords - // Removals per path, so a write can prove its source was not dropped under it - // rather than infer it from the cursor. In memory is enough: one process owns - // the index, and a removal only has to fence writes this process opened. - private readonly removals = new Map() + private readonly activeWrites = new Map() + private closed = false constructor( private readonly db: SyncDatabase, @@ -144,6 +144,9 @@ export class SessionSearchIndexWriter { previousByteOffset: number, identity?: () => TranscriptSessionIdentity | null ): SessionSearchFileWrite | null { + if (this.closed) { + return null + } const path = candidate.file.path const cursor = this.cursor(path) if (mode === 'append') { @@ -170,7 +173,11 @@ export class SessionSearchIndexWriter { * that is still in flight is fenced by the cursor its commit re-reads. */ removeFile(path: string): void { - this.removals.set(path, (this.removals.get(path) ?? 0) + 1) + const active = this.activeWrites.get(path) + if (active) { + active.removed = true + this.activeWrites.delete(path) + } const cursor = this.cursor(path) this.db.exec('BEGIN IMMEDIATE') try { @@ -183,6 +190,14 @@ export class SessionSearchIndexWriter { } } + close(): void { + this.closed = true + for (const active of this.activeWrites.values()) { + active.removed = true + } + this.activeWrites.clear() + } + private cursor(path: string): FileCursor | undefined { return this.db .prepare('SELECT session_row_id,byte_offset FROM files WHERE path = ?') @@ -204,11 +219,26 @@ export class SessionSearchIndexWriter { // these rows no longer continue anything, and committing on top of that // would resurrect a deleted source or duplicate a span. let expected = opened - const removalsAtStart = this.removals.get(path) ?? 0 // The session row is reused across re-reads of one file, so a `replace` // swaps a session's rows rather than minting a second generation of it. let session = opened?.session_row_id ?? null let hash = append && session !== null ? this.records.contentHash(session) : EMPTY_CONTENT_HASH + const lifetime = this.activeWrites.get(path) ?? { removed: false, readers: 0 } + lifetime.readers++ + this.activeWrites.set(path, lifetime) + let released = false + const discard = (): void => { + if (released) { + return + } + released = true + buffer.length = 0 + bufferedChars = 0 + lifetime.readers-- + if (lifetime.readers === 0 && this.activeWrites.get(path) === lifetime) { + this.activeWrites.delete(path) + } + } // A replace owns the session's whole row set, so the old generation goes in // the same transaction as the first of the new one. Chunk two onwards must // not repeat it. @@ -232,11 +262,9 @@ export class SessionSearchIndexWriter { // transaction it already knows will roll back, once per remaining message. let fenced = false - // Why a counter and not the cursor alone: on a path this index never wrote, - // `expected` and the absent row are both undefined, so the cursor compare - // reads a removal as no change and the write recreates the source. + // A missing cursor cannot distinguish a first read from its removed source. const current = (): boolean => { - if ((this.removals.get(path) ?? 0) !== removalsAtStart) { + if (lifetime.removed) { return false } const row = this.cursor(path) @@ -320,7 +348,8 @@ export class SessionSearchIndexWriter { return { add: (message) => { - if (fenced) { + if (released || fenced || this.closed) { + discard() return } hash = foldContentHash(hash, [message]) @@ -341,13 +370,19 @@ export class SessionSearchIndexWriter { const named = identity?.() ?? null if (named && !write(null, named)) { fenced = true - buffer.length = 0 - bufferedChars = 0 + discard() return } } }, - commit: (outcome) => !fenced && write(outcome, null) + commit: (outcome) => { + try { + return !released && !fenced && !this.closed && write(outcome, null) + } finally { + discard() + } + }, + discard } } diff --git a/src/main/ai-vault-search/session-search-store.ts b/src/main/ai-vault-search/session-search-store.ts index 73349970b1b..73da2c2db6f 100644 --- a/src/main/ai-vault-search/session-search-store.ts +++ b/src/main/ai-vault-search/session-search-store.ts @@ -361,6 +361,7 @@ export class SessionSearchStore { return } this.closed = true + this.writer.close() this.db.close() } } diff --git a/src/main/ai-vault-search/session-search-write-lifetime.test.ts b/src/main/ai-vault-search/session-search-write-lifetime.test.ts new file mode 100644 index 00000000000..ef0007b58ce --- /dev/null +++ b/src/main/ai-vault-search/session-search-write-lifetime.test.ts @@ -0,0 +1,209 @@ +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { join } from 'node:path' +import { TranscriptMessageChannel } from '../ai-vault/session-transcript-channel' +import { registerSessionSearchIndexConsumer } from './session-search-index-consumer' +import { SessionSearchIndexWriter } from './session-search-index-writer' +import { + openSessionSearchIndexFile, + syntheticCandidate, + syntheticSession, + type SessionSearchIndexFile +} from './session-search-index-test-fixture' +import { SessionSearchStore } from './session-search-store' + +let index: SessionSearchIndexFile +let store: SessionSearchStore +let writer: SessionSearchIndexWriter +let unregister: () => void +const message = { role: 'user' as const, text: 'needle', timestamp: null } +const outcome = { session: syntheticSession(), byteOffset: 100, incomplete: false } + +function trackedPaths(): number { + return writer['activeWrites'].size +} + +function openRead(named = true): TranscriptMessageChannel { + const channel = new TranscriptMessageChannel() + channel.beginRead({ + candidate: syntheticCandidate(), + mode: 'replace', + previousByteOffset: 0, + identity: named ? () => syntheticSession() : undefined + }) + return channel +} + +function failInsert(): void { + const prepare = index.db.prepare.bind(index.db) + vi.spyOn(index.db, 'prepare').mockImplementation((sql) => { + if (sql.startsWith('INSERT INTO sessions')) { + throw new Error('Synthetic insert failure') + } + return prepare(sql) + }) +} + +beforeEach(async () => { + index = await openSessionSearchIndexFile('ss-write-lifetime') + store = new SessionSearchStore(index.path) + writer = new SessionSearchIndexWriter(index.db, 1) + vi.spyOn(store, 'beginWrite').mockImplementation((...args) => writer.beginWrite(...args)) + unregister = registerSessionSearchIndexConsumer(store) +}) + +afterEach(async () => { + unregister() + writer.close() + vi.restoreAllMocks() + store.close() + await index.close() +}) + +it('retains no path metadata for repeated deletions without active writes', () => { + for (let index = 0; index < 1000; index++) { + writer.removeFile(join('synthetic', `retired-${index}.jsonl`)) + } + expect(trackedPaths()).toBe(0) +}) + +it('keeps the fence across intermediate chunks and releases it after final commit', () => { + const write = writer.beginWrite(syntheticCandidate(), 'replace', 0, () => syntheticSession())! + write.add(message) + expect(index.db.prepare('SELECT count(*) AS n FROM messages').get()).toMatchObject({ n: 1 }) + expect(trackedPaths()).toBe(1) + expect(write.commit(outcome)).toBe(true) + expect(trackedPaths()).toBe(0) + write.discard() + write.discard() + expect(write.commit(outcome)).toBe(false) + expect(trackedPaths()).toBe(0) +}) + +it('keeps concurrent reads fenced until each ends', () => { + const first = writer.beginWrite(syntheticCandidate(), 'replace', 0)! + const second = writer.beginWrite(syntheticCandidate(), 'replace', 0)! + first.discard() + first.discard() + expect(trackedPaths()).toBe(1) + writer.removeFile(syntheticCandidate().file.path) + second.add(message) + expect(second.commit(outcome)).toBe(false) + expect(trackedPaths()).toBe(0) + expect(index.db.prepare('SELECT count(*) AS n FROM files').get()).toMatchObject({ n: 0 }) +}) + +it('preserves the new generation when an older removed read finishes', () => { + const candidate = syntheticCandidate() + const old = writer.beginWrite(candidate, 'replace', 0)! + writer.removeFile(candidate.file.path) + expect(trackedPaths()).toBe(0) + const current = writer.beginWrite(candidate, 'replace', 0)! + old.discard() + expect(trackedPaths()).toBe(1) + writer.removeFile(candidate.file.path) + current.add(message) + expect(current.commit(outcome)).toBe(false) + expect(trackedPaths()).toBe(0) + expect(index.db.prepare('SELECT count(*) AS n FROM files').get()).toMatchObject({ n: 0 }) +}) + +it('permits a fresh read after removal while still refusing an older commit', () => { + const candidate = syntheticCandidate() + const old = writer.beginWrite(candidate, 'replace', 0)! + writer.removeFile(candidate.file.path) + const current = writer.beginWrite(candidate, 'replace', 0)! + old.add(message) + expect(old.commit(outcome)).toBe(false) + expect(trackedPaths()).toBe(1) + current.add(message) + expect(current.commit(outcome)).toBe(true) + expect(trackedPaths()).toBe(0) +}) + +it('releases a write when its final transaction throws', () => { + const write = writer.beginWrite(syntheticCandidate(), 'replace', 0)! + write.add(message) + failInsert() + expect(() => write.commit(outcome)).toThrow('Synthetic insert failure') + expect(trackedPaths()).toBe(0) + write.discard() + expect(trackedPaths()).toBe(0) +}) + +it('discards an incomplete consumer read without publishing its buffer', () => { + const channel = openRead(false) + channel.push(message) + expect(trackedPaths()).toBe(1) + channel.finishRead({ session: null, byteOffset: 0, incomplete: true }) + expect(trackedPaths()).toBe(0) + expect(index.db.prepare('SELECT count(*) AS n FROM messages').get()).toMatchObject({ n: 0 }) + expect(index.db.prepare('SELECT state FROM files').get()).toMatchObject({ state: 'failed' }) +}) + +it.each([false, true])( + 'keeps removed content absent when a consumer finishes, chunked: %s', + (chunked) => { + const channel = openRead(chunked) + channel.push(message) + writer.removeFile(syntheticCandidate().file.path) + expect(index.db.prepare('SELECT count(*) AS n FROM files').get()).toMatchObject({ n: 0 }) + channel.finishRead(outcome) + expect(trackedPaths()).toBe(0) + expect(index.db.prepare('SELECT count(*) AS n FROM sessions').get()).toMatchObject({ n: 0 }) + expect(index.db.prepare('SELECT count(*) AS n FROM messages').get()).toMatchObject({ n: 0 }) + // Existing retry bookkeeping may recreate a failed file row, never its searchable content. + expect(index.db.prepare('SELECT state FROM files').get()).toMatchObject({ state: 'failed' }) + } +) + +it.each([false, true])( + 'releases a failed consumer even when the reporter throws: %s', + (reporterThrows) => { + vi.spyOn(store, 'reportWriteFailure').mockImplementation(() => { + if (reporterThrows) { + throw new Error('Synthetic reporter failure') + } + }) + const channel = openRead() + failInsert() + expect(() => channel.push(message)).not.toThrow() + expect(channel.active).toBe(!reporterThrows) + expect(trackedPaths()).toBe(0) + channel.finishRead(outcome) + expect(trackedPaths()).toBe(0) + } +) + +it('discards after a finish failure even if the error reporter throws', () => { + const channel = new TranscriptMessageChannel() + channel.beginRead({ candidate: syntheticCandidate(), mode: 'replace', previousByteOffset: 0 }) + channel.push(message) + vi.spyOn(store, 'reportWriteFailure').mockImplementation(() => { + throw new Error('Synthetic reporter failure') + }) + failInsert() + expect(() => channel.finishRead(outcome)).not.toThrow() + expect(channel.active).toBe(false) + expect(trackedPaths()).toBe(0) +}) + +it('invalidates every write on close and refuses later writes', () => { + const write = writer.beginWrite(syntheticCandidate(), 'replace', 0)! + write.add(message) + writer.close() + writer.close() + expect(trackedPaths()).toBe(0) + expect(() => write.add(message)).not.toThrow() + expect(write.commit(outcome)).toBe(false) + expect(writer.beginWrite(syntheticCandidate(), 'replace', 0)).toBeNull() + expect(index.db.prepare('SELECT count(*) AS n FROM files').get()).toMatchObject({ n: 0 }) +}) + +it('closes the owned writer before closing the store database', () => { + vi.mocked(store.beginWrite).mockRestore() + const write = store.beginWrite(syntheticCandidate(), 'replace', 0)! + write.add(message) + store.close() + expect(() => write.add(message)).not.toThrow() + expect(write.commit(outcome)).toBe(false) +}) From 54e11473a6d5ca53e7b6e1eabdcf99e28af5c1f9 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:27:27 -0700 Subject: [PATCH 25/59] fix(browser): fence late registration replies to their guest owner (#21012) Co-authored-by: m4air --- .../README.md | 30 ++ .../fix.patch | 126 ++++++ .../reproduce.mjs | 153 ++++++++ .../results.json | 58 +++ .../host-guest/browser-page-guest-recovery.ts | 2 + ...rowser-page-registration-ownership.test.ts | 361 ++++++++++++++++++ .../browser-page-webview-guest-session.ts | 44 ++- 7 files changed, 766 insertions(+), 8 deletions(-) create mode 100644 docs/audits/browser-registration-reply-retention/README.md create mode 100644 docs/audits/browser-registration-reply-retention/fix.patch create mode 100644 docs/audits/browser-registration-reply-retention/reproduce.mjs create mode 100644 docs/audits/browser-registration-reply-retention/results.json create mode 100644 src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts diff --git a/docs/audits/browser-registration-reply-retention/README.md b/docs/audits/browser-registration-reply-retention/README.md new file mode 100644 index 00000000000..413fa8ed270 --- /dev/null +++ b/docs/audits/browser-registration-reply-retention/README.md @@ -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. diff --git a/docs/audits/browser-registration-reply-retention/fix.patch b/docs/audits/browser-registration-reply-retention/fix.patch new file mode 100644 index 00000000000..42ac361b59d --- /dev/null +++ b/docs/audits/browser-registration-reply-retention/fix.patch @@ -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 + } | null = null +- const registerGuest = (): Promise => { +- 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 => { ++ 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() diff --git a/docs/audits/browser-registration-reply-retention/reproduce.mjs b/docs/audits/browser-registration-reply-retention/reproduce.mjs new file mode 100644 index 00000000000..a851be61f91 --- /dev/null +++ b/docs/audits/browser-registration-reply-retention/reproduce.mjs @@ -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 }) +} diff --git a/docs/audits/browser-registration-reply-retention/results.json b/docs/audits/browser-registration-reply-retention/results.json new file mode 100644 index 00000000000..f3813295241 --- /dev/null +++ b/docs/audits/browser-registration-reply-retention/results.json @@ -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 +} 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 45f3b354b5f..b6e30917d12 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-registration-ownership.test.ts b/src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts new file mode 100644 index 00000000000..22ef6137de8 --- /dev/null +++ b/src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts @@ -0,0 +1,361 @@ +// @vitest-environment happy-dom +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { BrowserLoadError } from '../../../../../shared/browser-workspace-types' +import { + createBrowserPageWebviewGuestSession, + type BrowserPageWebviewGuestSession +} from './browser-page-webview-guest-session' +import { + destroyPersistentWebview, + registerPersistentWebview, + registeredWebContentsIds, + webviewRegistry +} from './webview-registry' + +vi.mock('../describe-page/browser-page-load-error', () => ({ browserPageExists: () => true })) + +function deferred() { + let resolve!: (value: T) => void + let reject!: (error: Error) => void + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise + reject = rejectPromise + }) + return { resolve, reject, promise } +} +const replies: ReturnType>[] = [] +const registrations = vi.fn(() => { + const reply = deferred() + replies.push(reply) + return reply.promise +}) +const isRegistered = vi.fn(async () => true) +const repair = vi.fn(async () => true) +const unregister = vi.fn(async () => true) +type RegistrationTestPage = { + id: string + webview: Electron.WebviewTag + webviewRef: { current: Electron.WebviewTag | null } + session: BrowserPageWebviewGuestSession + sync: ReturnType + update: ReturnType + pending: { current: boolean } + paintable: { current: boolean } + loadFailure: { current: BrowserLoadError | null } + setId: (next: number) => void +} +const sessions: RegistrationTestPage[] = [] + +function createWebview(): Electron.WebviewTag { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture installs the Electron webview methods exercised by guest registration and teardown on this DOM element. + return document.createElement('webview') as Electron.WebviewTag +} + +function createSession( + id: string, + guestId: number, + previous?: RegistrationTestPage +): RegistrationTestPage { + const webview = previous?.webview ?? createWebview() + let liveGuestId = guestId + webview.getWebContentsId = () => liveGuestId + webview.getZoomLevel = () => 0 + webview.setZoomLevel = vi.fn() + if (!previous) { + document.body.appendChild(webview) + registerPersistentWebview(id, webview) + } + const ref = (current: T) => ({ current }) + const webviewRef = previous?.webviewRef ?? ref(webview) + webviewRef.current = webview + const sync = vi.fn() + const update = vi.fn() + const pending = ref(false) + const paintable = ref(true) + const loadFailure = ref(null) + const session = createBrowserPageWebviewGuestSession({ + webview, + browserTabId: id, + workspaceId: 'browser-1', + worktreeId: 'wt-1', + sessionProfileId: null, + webviewRef, + isPaintableRef: paintable, + guestRecoveryPendingRef: pending, + browserTabUrlRef: ref('https://example.test'), + addressBarValueRef: ref('https://example.test'), + activeLoadFailureRef: loadFailure, + recoveryNavigationValidationRef: ref(null), + keepAddressBarFocusRef: ref(false), + paneZoomLevelRef: ref(0), + viewportPresetIdRef: ref(null), + onUpdatePageStateRef: ref(update), + setGuestRecoveryGeneration: vi.fn(), + setBrowserZoomPercent: vi.fn(), + focusAddressBarNow: () => false, + syncNavigationState: vi.fn(), + syncBrowserAnnotationViewportBridge: sync + }) + const result = { + id, + webview, + webviewRef, + session, + sync, + update, + pending, + paintable, + loadFailure, + setId: (next: number) => { + liveGuestId = next + } + } + sessions.push(result) + return result +} + +async function flush() { + await new Promise((resolve) => window.setTimeout(resolve, 0)) +} + +beforeEach(() => { + registrations.mockClear() + isRegistered.mockReset().mockResolvedValue(true) + repair.mockReset().mockResolvedValue(true) + unregister.mockClear() + Object.defineProperty(window, 'api', { + configurable: true, + value: { + browser: { + registerGuest: registrations, + unregisterGuest: unregister, + isGuestRegistered: isRegistered, + repairGuestRegistration: repair, + setViewportOverride: vi.fn(async () => true) + } + } + }) +}) + +afterEach(async () => { + for (const reply of replies.splice(0)) { + reply.resolve(false) + } + for (const page of sessions.splice(0)) { + page.session.guestRecovery.dispose() + page.webviewRef.current = null + await destroyPersistentWebview(page.id) + page.webview.remove() + } + registeredWebContentsIds.clear() +}) + +describe('renderer registration completion ownership', () => { + it('does not restore 1000 closed IDs from delayed successful replies', async () => { + for (let i = 0; i < 1000; i++) { + const page = createSession(`closed-${i}`, i + 1) + page.session.handleDidAttach() + page.session.guestRecovery.dispose() + page.webviewRef.current = null + await destroyPersistentWebview(page.id) + } + for (const reply of replies) { + reply.resolve(true) + } + await flush() + expect(webviewRegistry.size).toBe(0) + expect(registeredWebContentsIds.size).toBe(0) + expect(sessions.reduce((count, page) => count + page.sync.mock.calls.length, 0)).toBe(0) + expect(unregister).toHaveBeenCalledTimes(1000) + }) + + it('keeps the replacement ID after an older reply arrives', async () => { + const old = createSession('page', 1) + old.session.handleDidAttach() + old.session.guestRecovery.dispose() + await destroyPersistentWebview('page') + const replacement = createSession('page', 2) + replacement.session.handleDidAttach() + replies[1].resolve(true) + await flush() + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.get('page')).toBe(2) + expect(old.sync).not.toHaveBeenCalled() + expect(replacement.sync).toHaveBeenCalledOnce() + expect(unregister).toHaveBeenCalledOnce() + }) + + it('keeps a new ID when the same DOM webview swaps its guest', async () => { + const page = createSession('page', 1) + page.session.handleDidAttach() + page.setId(2) + page.session.handleDidAttach() + replies[1].resolve(true) + await flush() + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.get('page')).toBe(2) + expect(page.sync).toHaveBeenCalledOnce() + expect(unregister).not.toHaveBeenCalled() + }) + + it('a new session retries a disposed session registration on the same persistent guest and ref', async () => { + const old = createSession('page', 1) + old.session.handleDidAttach() + old.session.guestRecovery.dispose() + old.webviewRef.current = null + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.has('page')).toBe(false) + expect(webviewRegistry.get('page')).toBe(old.webview) + const replacement = createSession('page', 1, old) + replacement.session.guestRecovery.validateAfterResume() + expect(registrations).toHaveBeenCalledTimes(2) + replies[1].resolve(true) + await flush() + expect(registeredWebContentsIds.get('page')).toBe(1) + expect(old.sync).not.toHaveBeenCalled() + expect(unregister).not.toHaveBeenCalled() + }) + + it('a disposed listener cannot act after the same guest and ref are reused', async () => { + const old = createSession('page', 1) + old.session.handleDomReady() + old.session.guestRecovery.dispose() + const replacement = createSession('page', 1, old) + replacement.pending.current = true + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.has('page')).toBe(false) + expect(old.sync).not.toHaveBeenCalled() + expect(replacement.pending.current).toBe(true) + }) + + it.each(['attach', 'ready'] as const)('keeps successful current %s replies', async (event) => { + const page = createSession('page', 1) + if (event === 'attach') { + page.session.handleDidAttach() + } else { + page.session.handleDomReady() + } + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.get('page')).toBe(1) + expect(page.sync).toHaveBeenCalledOnce() + expect(unregister).not.toHaveBeenCalled() + }) + + it.each(['false', 'reject'] as const)( + 'preserves inconclusive current registration %s', + async (result) => { + const page = createSession('page', 1) + page.session.handleDidAttach() + if (result === 'false') { + replies[0].resolve(false) + } else { + replies[0].reject(new Error('attach race')) + } + await flush() + expect(registeredWebContentsIds.has('page')).toBe(false) + expect(page.sync).toHaveBeenCalledOnce() + expect(unregister).not.toHaveBeenCalled() + } + ) + + it('does not restore a registry-removed guest before listener disposal runs', async () => { + const page = createSession('page', 1) + page.session.handleDidAttach() + await destroyPersistentWebview('page') + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.has('page')).toBe(false) + expect(page.sync).not.toHaveBeenCalled() + }) + + it('does not restore metadata when the current listener ref has moved', async () => { + const page = createSession('page', 1) + page.session.handleDidAttach() + page.webviewRef.current = null + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.has('page')).toBe(false) + expect(page.sync).not.toHaveBeenCalled() + expect(webviewRegistry.get('page')).toBe(page.webview) + expect(unregister).not.toHaveBeenCalled() + }) + + it('accepts a current registration while its persistent guest is hidden', async () => { + const page = createSession('page', 1) + page.paintable.current = false + page.session.handleDidAttach() + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.get('page')).toBe(1) + expect(page.sync).toHaveBeenCalledOnce() + expect(unregister).not.toHaveBeenCalled() + }) + + it('ignores a reply after reading the guest identity starts throwing', async () => { + const page = createSession('page', 1) + page.session.handleDidAttach() + page.webview.getWebContentsId = () => { + throw new Error('guest detached') + } + replies[0].resolve(true) + await flush() + expect(registeredWebContentsIds.has('page')).toBe(false) + expect(page.sync).not.toHaveBeenCalled() + }) + + it('does not issue repair after a pending validation outlives its owner', async () => { + const reply = deferred() + isRegistered.mockReturnValue(reply.promise) + const page = createSession('page', 1) + registeredWebContentsIds.set('page', 1) + page.session.guestRecovery.validateAfterResume() + page.session.guestRecovery.dispose() + await destroyPersistentWebview('page') + reply.resolve(false) + await flush() + expect(repair).not.toHaveBeenCalled() + expect(unregister).toHaveBeenCalledOnce() + }) + + it('still repairs an inconclusive current registration', async () => { + isRegistered.mockResolvedValue(false) + const page = createSession('page', 1) + registeredWebContentsIds.set('page', 1) + page.session.guestRecovery.validateAfterResume() + await flush() + expect(repair).toHaveBeenCalledExactlyOnceWith({ + browserPageId: 'page', + workspaceId: 'browser-1', + worktreeId: 'wt-1', + sessionProfileId: null, + webContentsId: 1 + }) + }) + + it('does not clear a newer guest recovery error from a pending old repair reply', async () => { + const reply = deferred() + repair.mockReturnValue(reply.promise) + isRegistered.mockResolvedValue(false) + const page = createSession('page', 1) + registeredWebContentsIds.set('page', 1) + page.session.guestRecovery.validateAfterResume() + await flush() + expect(repair).toHaveBeenCalledOnce() + page.setId(2) + const failure = { + code: -10_000, + description: 'Replacement guest recovery failed', + validatedUrl: 'https://example.test' + } + page.loadFailure.current = failure + reply.resolve(true) + await flush() + expect(page.loadFailure.current).toBe(failure) + expect(page.update).not.toHaveBeenCalled() + }) +}) 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 4623b817b2a..dd5e243ba25 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 } | null = null - const registerGuest = (): Promise => { - 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 => { + 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() From b4a6e2a80aea16838264837ab2d471577690499f Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Fri, 18 Sep 2026 00:27:33 -0300 Subject: [PATCH 26/59] fix(filesystem): match allowed roots across Unicode forms (#21194) * fix(filesystem): match allowed roots across Unicode forms macOS returns a path in whichever Unicode form its source held: APFS gives back what it stores (NFD), while the file picker and git (core.precomposeunicode) give back NFC. A workspace registered in one form never matched a file read in the other, so fs:readFile denied a path inside the open workspace (#21172). isDescendantOrEqual now compares byte-exactly first and retries in NFC only when that fails and both sides carry non-ASCII, leaving ASCII containment and the traversal guards untouched. * fix(filesystem): prove identity before admitting a Unicode-folded root Canonical equivalence is not identity: APFS folds both spellings onto one directory, but a byte-exact filesystem can hold them as distinct siblings, and admitting the unregistered one widened the allow-list. The NFC fold now only locates the ancestor of the target that the registered root would have to be; containment is granted only when that ancestor and the root stat to the same dev+ino. A failed stat or an ino of 0 denies. ASCII paths and roots that do not fold onto the target never reach the disk. --- ...-containment-unicode-normalization.test.ts | 203 ++++++++++++++++++ src/main/ipc/filesystem-path-containment.ts | 82 ++++++- 2 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 src/main/ipc/filesystem-path-containment-unicode-normalization.test.ts diff --git a/src/main/ipc/filesystem-path-containment-unicode-normalization.test.ts b/src/main/ipc/filesystem-path-containment-unicode-normalization.test.ts new file mode 100644 index 00000000000..f5348061ab9 --- /dev/null +++ b/src/main/ipc/filesystem-path-containment-unicode-normalization.test.ts @@ -0,0 +1,203 @@ +import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' + +import type { Store } from '../persistence' +import type { Repo } from '../../shared/repo-types' +import { PATH_ACCESS_DENIED_MESSAGE, resolveAuthorizedPath } from './filesystem-auth' +import { isDescendantOrEqual } from './filesystem-path-containment' + +/** + * Opening any file under a Korean-named workspace failed on macOS with + * "Access denied: path resolves outside allowed directories" (#21172). + * + * The file was inside the workspace the whole time. The two sides of the containment check reached + * it through different doors: the root was registered from the file picker or from git, which spell + * the name in NFC, while the path being read came back from the filesystem in NFD. Same name, same + * file on APFS, different bytes — so the guard reported an escape. + * + * "Same file" is the load-bearing half, so it is the half that gets proven: both spellings reaching + * one directory authorize the file, two distinct directories — which ext4 allows and APFS does not + * — leave the unregistered one denied. A symlink stands in for the APFS fold on a byte-exact host. + */ + +const FOLDER = '테스트프로젝트' +const NFC_FOLDER = FOLDER.normalize('NFC') +const NFD_FOLDER = FOLDER.normalize('NFD') + +const scratchDirs: string[] = [] + +async function makeScratchDir(): Promise { + // realpath first: macOS fronts the temp dir with a /var symlink of its own. + const scratch = await mkdtemp(join(await realpath(tmpdir()), 'orca-unicode-path-')) + scratchDirs.push(scratch) + return scratch +} + +function isEEXIST(error: unknown): boolean { + return error instanceof Error && 'code' in error && error.code === 'EEXIST' +} + +/** One directory, both spellings. EEXIST means the filesystem already folded them itself. */ +async function makeOneDirectoryTwoSpellings( + scratch: string +): Promise<{ onDisk: string; registered: string }> { + const onDisk = join(scratch, NFD_FOLDER) + const registered = join(scratch, NFC_FOLDER) + await mkdir(onDisk) + try { + await symlink(onDisk, registered, 'dir') + } catch (error) { + if (!isEEXIST(error)) { + throw error + } + } + return { onDisk, registered } +} + +/** Two canonically equal names as two distinct directories; null where the filesystem folds them. */ +async function makeDistinctSiblings( + scratch: string +): Promise<{ registered: string; sibling: string } | null> { + const registered = join(scratch, NFC_FOLDER) + const sibling = join(scratch, NFD_FOLDER) + await mkdir(registered) + try { + await mkdir(sibling) + } catch (error) { + if (isEEXIST(error)) { + return null + } + throw error + } + return { registered, sibling } +} + +function makeStore(repoPath: string): Store { + const repo: Repo = { + id: 'repo-1', + path: repoPath, + displayName: 'workspace', + badgeColor: '#000000', + addedAt: 1, + kind: 'git' + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the guard reads only these four accessors; Store is a class, so a structural double cannot satisfy it without the cast. + return { + getRepos: () => [repo], + getProjectGroups: () => [], + getFolderWorkspaces: () => [], + getSettings: () => ({}) + } as unknown as Store +} + +afterEach(async () => { + await Promise.all(scratchDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +describe('path containment across Unicode forms', () => { + it('spells the fixture two ways, or the rest of this file proves nothing', () => { + expect(NFC_FOLDER).not.toBe(NFD_FOLDER) + }) + + it('accepts a child of a root the filesystem spells the other way', async () => { + const scratch = await makeScratchDir() + const { onDisk, registered } = await makeOneDirectoryTwoSpellings(scratch) + + expect(isDescendantOrEqual(join(onDisk, 'test.txt'), registered)).toBe(true) + }) + + it('accepts the root itself under its other spelling', async () => { + const scratch = await makeScratchDir() + const { onDisk, registered } = await makeOneDirectoryTwoSpellings(scratch) + + expect(isDescendantOrEqual(onDisk, registered)).toBe(true) + }) + + it('denies a canonically equal sibling that is a distinct directory', async () => { + const scratch = await makeScratchDir() + const siblings = await makeDistinctSiblings(scratch) + if (!siblings) { + // The filesystem folds the two names, so there is no second directory to reach. + return + } + + expect( + isDescendantOrEqual(join(siblings.sibling, 'test.txt'), siblings.registered), + 'the sibling is a different directory; the user opened only the registered one' + ).toBe(false) + }) + + it('denies a fold it cannot check against the filesystem', () => { + // Neither spelling exists on disk, so identity is unproven — and unproven is denied. + expect( + isDescendantOrEqual(resolve(`/repos/${NFD_FOLDER}/test.txt`), resolve(`/repos/${NFC_FOLDER}`)) + ).toBe(false) + }) + + it('still rejects a sibling that only looks similar', () => { + // 테스트 is a prefix of 테스트프로젝트, not a canonical equivalent of it. + expect( + isDescendantOrEqual(resolve('/repos/테스트/test.txt'), resolve(`/repos/${NFC_FOLDER}`)) + ).toBe(false) + }) + + it('still rejects an escape out of a non-ASCII root', () => { + expect( + isDescendantOrEqual( + resolve(`/repos/${NFD_FOLDER}/../secrets`), + resolve(`/repos/${NFC_FOLDER}`) + ) + ).toBe(false) + }) + + it('leaves ASCII containment exactly as it was', () => { + expect(isDescendantOrEqual(resolve('/repos/app/src'), resolve('/repos/app'))).toBe(true) + expect(isDescendantOrEqual(resolve('/repos/apple'), resolve('/repos/app'))).toBe(false) + expect(isDescendantOrEqual(resolve('/repos/app'), resolve('/repos/app'))).toBe(true) + }) +}) + +describe('fs:readFile authorization for a Korean-named workspace', () => { + it('authorizes a file the filesystem spells the other way', async () => { + const scratch = await makeScratchDir() + const { onDisk, registered } = await makeOneDirectoryTwoSpellings(scratch) + const file = join(onDisk, 'test.txt') + await writeFile(file, 'hello') + + const store = makeStore(registered) + // Resolved before the assertion so a rejection lands on expect(), not on an unawaited promise. + const expected = await realpath(file) + + await expect( + resolveAuthorizedPath(file, store), + 'the file is inside the opened workspace; only its spelling differs' + ).resolves.toBe(expected) + }) + + it('denies a canonically equal sibling the user never opened', async () => { + const scratch = await makeScratchDir() + const siblings = await makeDistinctSiblings(scratch) + if (!siblings) { + return + } + const file = join(siblings.sibling, 'test.txt') + await writeFile(file, 'secret') + + const store = makeStore(siblings.registered) + + await expect(resolveAuthorizedPath(file, store)).rejects.toThrow(PATH_ACCESS_DENIED_MESSAGE) + }) + + it('still denies a file outside the workspace', async () => { + const scratch = await makeScratchDir() + await makeOneDirectoryTwoSpellings(scratch) + const outside = join(scratch, 'outside.txt') + await writeFile(outside, 'secret') + + const store = makeStore(join(scratch, NFC_FOLDER)) + + await expect(resolveAuthorizedPath(outside, store)).rejects.toThrow(PATH_ACCESS_DENIED_MESSAGE) + }) +}) diff --git a/src/main/ipc/filesystem-path-containment.ts b/src/main/ipc/filesystem-path-containment.ts index 32d1c00f0e1..c696028cbc8 100644 --- a/src/main/ipc/filesystem-path-containment.ts +++ b/src/main/ipc/filesystem-path-containment.ts @@ -1,11 +1,23 @@ -import { resolve, relative, isAbsolute, sep } from 'node:path' +import { resolve, relative, isAbsolute, sep, dirname } from 'node:path' +import { statSync } from 'node:fs' import { realpath } from 'node:fs/promises' /** * Check whether resolvedTarget is equal to or a descendant of resolvedBase. * Uses relative() so it works with both `/` (Unix) and `\` (Windows) separators. + * + * Compared byte-exactly first, then across Unicode forms — but only where both spellings prove to + * be one filesystem object. */ export function isDescendantOrEqual(resolvedTarget: string, resolvedBase: string): boolean { + if (isDescendantOrEqualExact(resolvedTarget, resolvedBase)) { + return true + } + const ancestor = foldedContainmentAncestor(resolvedTarget, resolvedBase) + return ancestor !== null && isSameFilesystemObject(ancestor, resolvedBase) +} + +function isDescendantOrEqualExact(resolvedTarget: string, resolvedBase: string): boolean { if (resolvedTarget === resolvedBase) { return true } @@ -15,6 +27,74 @@ export function isDescendantOrEqual(resolvedTarget: string, resolvedBase: string return rel !== '' && !(rel === '..' || rel.startsWith(`..${sep}`)) && !isAbsolute(rel) } +/** + * Why a loop and not a regex: `[^\u0000-\u007f]` trips no-control-regex, and this runs once per + * registered root on every filesystem IPC, where charCodeAt beats an ICU-backed scan anyway. + */ +function hasNonAscii(value: string): boolean { + for (let index = 0; index < value.length; index += 1) { + if (value.charCodeAt(index) > 0x7f) { + return true + } + } + return false +} + +/** + * The same name, spelled two ways. + * + * macOS returns a path in whichever Unicode form its source held: APFS gives back the form it + * stores — NFD for names typed into Finder — while the file picker and git (`core.precomposeunicode`) + * give back NFC. A workspace whose path contains Korean, accented or otherwise composed characters + * is registered in one form and read in the other, so byte comparison puts the file outside its own + * workspace and fs:readFile denies a path the user is looking at (#21172). ASCII paths are immune, + * which is why the guard held for so long. + * + * Canonical equivalence is not containment on its own: APFS folds both spellings onto one + * directory, ext4 keeps them as distinct siblings, and the unregistered sibling is not the root — + * equivalence includes singletons such as U+212A KELVIN SIGN folding to K. So the fold only locates + * the candidate ancestor, in the caller's spelling; isDescendantOrEqual settles identity. + * + * Walks up by component count, never by offset: NFD is longer than NFC. + */ +function foldedContainmentAncestor(resolvedTarget: string, resolvedBase: string): string | null { + // Both sides must carry non-ASCII before normalize() earns its allocation: ASCII is identical in + // every form, so a mismatch confined to it is a real one. Target first — it is the side the + // allow-list scan holds fixed while it walks every registered root. + if (!hasNonAscii(resolvedTarget) || !hasNonAscii(resolvedBase)) { + return null + } + const foldedBase = resolvedBase.normalize('NFC') + const foldedTarget = resolvedTarget.normalize('NFC') + if (!isDescendantOrEqualExact(foldedTarget, foldedBase)) { + return null + } + const descent = relative(foldedBase, foldedTarget) + let ancestor = resolvedTarget + for (let depth = descent === '' ? 0 : descent.split(sep).length; depth > 0; depth -= 1) { + ancestor = dirname(ancestor) + } + return ancestor +} + +/** + * The same directory entry, not merely the same name — the question the fold is really asking, and + * only the filesystem can answer it. + * + * Fails closed: an ancestor that cannot be stat'ed has not shown it is the registered root, and ino + * is 0 on volumes that expose none. Reached only on a containment the exact comparison refused, so + * ASCII paths and non-folding roots still touch no disk. + */ +function isSameFilesystemObject(pathA: string, pathB: string): boolean { + try { + const statA = statSync(pathA) + const statB = statSync(pathB) + return statA.ino !== 0 && statA.dev === statB.dev && statA.ino === statB.ino + } catch { + return false + } +} + /** * Node's canonical ENOENT message. Matched in full so a message that merely mentions the word — a * log line, a user's branch name — cannot be mistaken for a missing path. From 41059f65b25d9ea1f67a3ed92ca795d98450a667 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:28:52 -0700 Subject: [PATCH 27/59] fix(pty): reconcile daemon exits after synthetic notifications (#21000) Co-authored-by: m4air --- docs/audits/daemon-late-exit/README.md | 70 ++++++ docs/audits/daemon-late-exit/reproduce.mjs | 120 +++++++++ docs/audits/daemon-late-exit/results.json | 236 ++++++++++++++++++ .../ipc/pty-runtime-kill-and-exit.test.ts | 11 +- .../ipc/pty/daemon-late-exit-test-fixture.ts | 211 ++++++++++++++++ src/main/ipc/pty/daemon-late-exit.test.ts | 235 +++++++++++++++++ src/main/ipc/pty/delivery/exit.ts | 24 +- src/main/ipc/pty/delivery/wire-session.ts | 6 +- src/main/ipc/pty/ipc/renderer-kill.ts | 6 +- src/main/ipc/pty/provider/bind-listeners.ts | 11 +- src/main/ipc/pty/runtime/controller-deps.ts | 2 +- src/main/ipc/pty/runtime/kill.ts | 17 +- src/main/ipc/pty/session.ts | 9 +- 13 files changed, 927 insertions(+), 31 deletions(-) create mode 100644 docs/audits/daemon-late-exit/README.md create mode 100644 docs/audits/daemon-late-exit/reproduce.mjs create mode 100644 docs/audits/daemon-late-exit/results.json create mode 100644 src/main/ipc/pty/daemon-late-exit-test-fixture.ts create mode 100644 src/main/ipc/pty/daemon-late-exit.test.ts diff --git a/docs/audits/daemon-late-exit/README.md b/docs/audits/daemon-late-exit/README.md new file mode 100644 index 00000000000..d1896f9f158 --- /dev/null +++ b/docs/audits/daemon-late-exit/README.md @@ -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. diff --git a/docs/audits/daemon-late-exit/reproduce.mjs b/docs/audits/daemon-late-exit/reproduce.mjs new file mode 100644 index 00000000000..b82ca0333d8 --- /dev/null +++ b/docs/audits/daemon-late-exit/reproduce.mjs @@ -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 }) +} diff --git a/docs/audits/daemon-late-exit/results.json b/docs/audits/daemon-late-exit/results.json new file mode 100644 index 00000000000..2dfd43a0353 --- /dev/null +++ b/docs/audits/daemon-late-exit/results.json @@ -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 + } + } + ] + } + ] +} diff --git a/src/main/ipc/pty-runtime-kill-and-exit.test.ts b/src/main/ipc/pty-runtime-kill-and-exit.test.ts index 2200b2b6043..05a6d67e2d4 100644 --- a/src/main/ipc/pty-runtime-kill-and-exit.test.ts +++ b/src/main/ipc/pty-runtime-kill-and-exit.test.ts @@ -294,10 +294,11 @@ describe('registerPtyHandlers', () => { [['pty:exit', { id: 'local-pty', code: 0 }]] ) }) - it('ignores a late provider exit after synthesizing kill exit', async () => { + it('reconciles a late provider exit without repeating the synthetic renderer exit', async () => { const exitListeners = new Set<(payload: { id: string; code: number }) => void>() const runtime = { setPtyController: vi.fn(), + markPtyStopRequested: vi.fn(), onPtyExit: vi.fn() } setLocalPtyProvider({ @@ -333,8 +334,12 @@ describe('registerPtyHandlers', () => { listener({ id: 'local-pty', code: 0 }) } - expect(runtime.onPtyExit).toHaveBeenCalledTimes(1) - expect(runtime.onPtyExit).toHaveBeenCalledWith('local-pty', -1, undefined) + expect(runtime.onPtyExit).toHaveBeenCalledTimes(2) + expect(runtime.onPtyExit).toHaveBeenNthCalledWith(1, 'local-pty', -1, undefined) + expect(runtime.onPtyExit).toHaveBeenNthCalledWith(2, 'local-pty', 0, undefined, { + providerExitObserved: true + }) + expect(runtime.markPtyStopRequested).toHaveBeenCalledTimes(2) expect(mainWindow.webContents.send.mock.calls.filter((call) => call[0] === 'pty:exit')).toEqual( [['pty:exit', { id: 'local-pty', code: -1 }]] ) diff --git a/src/main/ipc/pty/daemon-late-exit-test-fixture.ts b/src/main/ipc/pty/daemon-late-exit-test-fixture.ts new file mode 100644 index 00000000000..e9ca6f48111 --- /dev/null +++ b/src/main/ipc/pty/daemon-late-exit-test-fixture.ts @@ -0,0 +1,211 @@ +import type { BrowserWindow } from 'electron' +import { Socket } from 'node:net' +import { rmSync } from 'node:fs' +import { + createMockSubprocess, + startDaemonAdapterHarness, + waitFor +} from '../../daemon/daemon-pty-adapter-test-harness' +import { OrcaRuntimeService } from '../../runtime/orca-runtime' +import { setPtyHostBindings, type PtyIpcSurface } from '../pty-host-bindings' +import { consumeSyntheticKillExit, rememberSyntheticKillExit } from './delivery/exit' +import { installPtyKillIpcHandler } from './ipc/renderer-kill' +import { bindProviderListeners } from './provider/bind-listeners' +import { ptyIncarnationById, ptyOwnership } from './provider/ownership-state' +import { getLocalPtyProvider, setLocalPtyProvider } from './provider/registry' +import { unbindLocalProviderListeners } from './provider/listener-lifecycle' +import { shutdownProviderAndDetectExit } from './provider/shutdown-detect' +import { finishPtyShutdown } from './provider/liveness' +import { stopAndWaitPtyFromRuntimeController } from './runtime/kill' +import type { PtyRuntimeControllerDeps } from './runtime/controller-deps' +import { createPtyIpcSession } from './session' +import type { DaemonPtyAdapter } from '../../daemon/daemon-pty-adapter' + +const PTY_ID = 'repo::/tmp/late-exit-audit@@terminal' +const TAB_ID = '00000000-0000-4000-8000-000000000001' +const LEAF_ID = '00000000-0000-4000-8000-000000000002' + +class LateExitRuntime extends OrcaRuntimeService { + observeExit(listener: () => void): void { + this.ptyExitListenersByPtyId.set(PTY_ID, new Set([listener])) + } + + captureState() { + const pty = this.ptysById.get(PTY_ID) + return { + connected: pty?.connected, + exitCause: pty?.lastExitCause, + incarnationId: pty?.incarnationId, + headlessModelRetained: this.headlessTerminals.has(PTY_ID), + titleTrackerRetained: this.ptyTitleTrackersByPtyId.has(PTY_ID), + liveness: this.ptyLivenessVerdictByPtyId.get(PTY_ID)?.verdict.status ?? null + } + } +} + +function adapterStreamSocket(adapter: DaemonPtyAdapter): Socket { + const socket = adapter['client']['streamSocket'] + if (!(socket instanceof Socket)) { + throw new Error('Daemon stream socket missing') + } + return socket +} + +export async function startLateExitHarness() { + let subprocess = createMockSubprocess() + const harness = await startDaemonAdapterHarness(() => { + subprocess = createMockSubprocess() + return subprocess + }) + const runtime = new LateExitRuntime() + const priorProvider = getLocalPtyProvider() + const deliveredData: string[] = [] + const rendererExits: { id: string; code: number; incarnationId?: string }[] = [] + const providerExits: { id: string; code: number; incarnationId?: string }[] = [] + let exitListenerCalls = 0 + const windowStub = { isDestroyed: () => false, webContents: { send() {} } } + const session = createPtyIpcSession({ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: provider listeners only inspect isDestroyed and webContents.send on this headless fixture. + mainWindow: windowStub as unknown as BrowserWindow, + runtime + }) + session.acceptPtyDataForRenderer = (event) => { + deliveredData.push(event.data) + } + session.sendPtyExitToRenderer = (event) => { + rendererExits.push(event) + } + session.consumeSyntheticKillExit = (id, incarnationId) => + consumeSyntheticKillExit(session, id, incarnationId) + session.rememberSyntheticKillExit = (id, incarnationId) => + rememberSyntheticKillExit(session, id, incarnationId) + session.sendModelRestoreNeededMarker = () => false + let killHandler: Parameters[1] | undefined + setLocalPtyProvider(harness.adapter) + bindProviderListeners(session) + setPtyHostBindings({ + ipc: { + handle: (channel, handler) => { + if (channel === 'pty:kill') { + killHandler = handler + } + }, + on() {}, + removeHandler() {}, + removeAllListeners() {} + } + }) + installPtyKillIpcHandler({ + runtime, + getLocalPtyProviderStartupPromise: () => undefined, + shutdownProviderAndDetectExit, + rememberSyntheticKillExit: session.rememberSyntheticKillExit, + sendPtyExitToRenderer: session.sendPtyExitToRenderer + }) + const result = await harness.adapter.spawn({ cols: 80, rows: 24, sessionId: PTY_ID }) + ptyOwnership.set(PTY_ID, null) + if (result.incarnationId) { + ptyIncarnationById.set(PTY_ID, result.incarnationId) + } + runtime.registerPty(PTY_ID, 'repo::/tmp/late-exit-audit', null, { + tabId: TAB_ID, + leafId: LEAF_ID, + incarnationId: result.incarnationId + }) + runtime.observeExit(() => { + exitListenerCalls++ + }) + harness.adapter.onExit((event) => { + providerExits.push(event) + }) + const streamSocket = adapterStreamSocket(harness.adapter) + return { + ...harness, + runtime, + session, + result, + deliveredData, + rendererExits, + providerExits, + get subprocess() { + return subprocess + }, + respawn: async () => { + harness.adapter.clearTombstone(PTY_ID) + const replacement = await harness.adapter.spawn({ cols: 80, rows: 24, sessionId: PTY_ID }) + ptyOwnership.set(PTY_ID, null) + if (replacement.incarnationId) { + ptyIncarnationById.set(PTY_ID, replacement.incarnationId) + } + runtime.registerPty(PTY_ID, 'repo::/tmp/late-exit-audit', null, { + tabId: TAB_ID, + leafId: LEAF_ID, + incarnationId: replacement.incarnationId + }) + runtime.observeExit(() => { + exitListenerCalls++ + }) + return replacement + }, + id: PTY_ID, + deliverProviderExit: (event: { id: string; code: number; incarnationId?: string }) => { + // oxlint-disable-next-line unicorn/no-useless-spread -- copy-safe: exit listeners may unsubscribe while receiving the event. + for (const listener of [...harness.adapter['exitListeners']]) { + listener(event) + } + }, + pauseStream: () => { + streamSocket.pause() + }, + resumeStream: () => { + streamSocket.resume() + }, + kill: async () => { + if (!killHandler) { + throw new Error('PTY kill handler missing') + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the installed renderer-kill handler ignores its Electron event argument. + await killHandler({} as never, { id: PTY_ID }) + }, + stopAndWait: () => { + const deps = { + runtime, + getLocalPtyProviderStartupPromise: () => undefined, + shutdownProviderAndDetectExit, + rememberSyntheticKillExit: session.rememberSyntheticKillExit, + sendPtyExitToRenderer: session.sendPtyExitToRenderer, + finishPtyShutdown + } + return stopAndWaitPtyFromRuntimeController( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: exact-stop reads only these seven controller ports and optional store; unrelated spawn ports are unused. + deps as unknown as PtyRuntimeControllerDeps, + PTY_ID + ) + }, + waitForExit: () => waitFor(() => providerExits.length > 0), + capture: async () => ({ + ...runtime.captureState(), + providerHasPty: harness.adapter.hasPty(PTY_ID), + hostInventoryCount: (await harness.adapter.listProcesses()).length, + deliveredData: [...deliveredData], + rendererExitCount: rendererExits.length, + providerExitCount: providerExits.length, + exitListenerCalls + }), + dispose: async () => { + for (const pending of session.syntheticKillExitPtyIds.values()) { + clearTimeout(pending.cleanupTimer) + } + session.syntheticKillExitPtyIds.clear() + runtime.onPtyExit(PTY_ID, 0, runtime.captureState().incarnationId ?? undefined) + unbindLocalProviderListeners() + harness.adapter.dispose() + await harness.server.shutdown() + setLocalPtyProvider(priorProvider) + setPtyHostBindings({}) + ptyOwnership.delete(PTY_ID) + ptyIncarnationById.delete(PTY_ID) + rmSync(harness.dir, { recursive: true, force: true }) + } + } +} diff --git a/src/main/ipc/pty/daemon-late-exit.test.ts b/src/main/ipc/pty/daemon-late-exit.test.ts new file mode 100644 index 00000000000..7b854ce735c --- /dev/null +++ b/src/main/ipc/pty/daemon-late-exit.test.ts @@ -0,0 +1,235 @@ +import { describe, expect, it } from 'vitest' +import { startLateExitHarness } from './daemon-late-exit-test-fixture' + +const FINAL_OUTPUT = 'delayed final output\r\n' + +describe('daemon physical exit after synthetic renderer exit', () => { + it('retires delayed output after the kill control reply overtakes the stream', async () => { + const harness = await startLateExitHarness() + try { + harness.pauseStream() + harness.subprocess._simulateData(FINAL_OUTPUT) + await harness.kill() + expect(harness.runtime.captureState().connected).toBe(false) + expect(harness.providerExits).toHaveLength(0) + expect(await harness.adapter.listProcesses()).toEqual([]) + harness.resumeStream() + await harness.waitForExit() + expect(await harness.capture()).toMatchObject({ + connected: false, + headlessModelRetained: false, + titleTrackerRetained: false, + providerHasPty: false, + hostInventoryCount: 0, + deliveredData: [FINAL_OUTPUT], + rendererExitCount: 1, + providerExitCount: 1, + exitListenerCalls: 1, + exitCause: { kind: 'operator_close' } + }) + } finally { + await harness.dispose() + } + }) + + it('does not revive a process already proven exited by fresh host inventory', async () => { + const harness = await startLateExitHarness() + try { + harness.pauseStream() + harness.subprocess._simulateData(FINAL_OUTPUT) + expect(await harness.stopAndWait()).toBe(true) + expect(harness.runtime.captureState()).toMatchObject({ connected: false, liveness: 'exited' }) + harness.resumeStream() + await harness.waitForExit() + expect(await harness.capture()).toMatchObject({ + connected: false, + liveness: 'exited', + headlessModelRetained: false, + deliveredData: [FINAL_OUTPUT], + rendererExitCount: 1, + exitListenerCalls: 1, + exitCause: { kind: 'operator_close' } + }) + } finally { + await harness.dispose() + } + }) + + it('reconciles physical exit without another renderer notification when no data was queued', async () => { + const harness = await startLateExitHarness() + try { + harness.pauseStream() + await harness.kill() + harness.resumeStream() + await harness.waitForExit() + expect(await harness.capture()).toMatchObject({ + connected: false, + headlessModelRetained: false, + deliveredData: [], + rendererExitCount: 1, + providerExitCount: 1, + exitListenerCalls: 1 + }) + } finally { + await harness.dispose() + } + }) + + it('preserves ordinary stream-ordered output and natural exit', async () => { + const harness = await startLateExitHarness() + try { + harness.subprocess._simulateData(FINAL_OUTPUT) + harness.subprocess._simulateExit(0) + await harness.waitForExit() + expect(await harness.capture()).toMatchObject({ + connected: false, + headlessModelRetained: false, + deliveredData: [FINAL_OUTPUT], + rendererExitCount: 1, + providerExitCount: 1, + exitListenerCalls: 1, + exitCause: { kind: 'exited', exitCode: 0 } + }) + } finally { + await harness.dispose() + } + }) + + it('does not suppress the replacement incarnation exit with its predecessor marker', async () => { + const harness = await startLateExitHarness() + try { + harness.pauseStream() + await harness.kill() + const replacement = await harness.respawn() + expect(replacement.id).toBe(harness.id) + expect(replacement.incarnationId).not.toBe(harness.result.incarnationId) + harness.subprocess._simulateData(FINAL_OUTPUT) + harness.subprocess._simulateExit(0) + harness.resumeStream() + await harness.waitForExit() + expect(await harness.capture()).toMatchObject({ + incarnationId: replacement.incarnationId, + connected: false, + headlessModelRetained: false, + deliveredData: [FINAL_OUTPUT], + rendererExitCount: 2, + providerExitCount: 1, + exitListenerCalls: 2, + exitCause: { kind: 'exited', exitCode: 0 } + }) + } finally { + await harness.dispose() + } + }) + + it('keeps a new synthetic marker when the old incarnation exit arrives first', async () => { + const harness = await startLateExitHarness() + try { + harness.pauseStream() + await harness.kill() + const replacement = await harness.respawn() + harness.subprocess._simulateData(FINAL_OUTPUT) + await harness.kill() + expect(harness.session.syntheticKillExitPtyIds.get(harness.id)?.incarnationId).toBe( + replacement.incarnationId + ) + harness.resumeStream() + await harness.waitForExit() + expect(await harness.capture()).toMatchObject({ + incarnationId: replacement.incarnationId, + connected: false, + headlessModelRetained: false, + deliveredData: [FINAL_OUTPUT], + rendererExitCount: 2, + providerExitCount: 1, + exitListenerCalls: 2, + exitCause: { kind: 'operator_close' } + }) + expect(harness.session.syntheticKillExitPtyIds.has(harness.id)).toBe(false) + } finally { + await harness.dispose() + } + }) + + it('rejects a stale provider exit before touching replacement state or its marker', async () => { + const harness = await startLateExitHarness() + try { + harness.pauseStream() + await harness.kill() + const replacement = await harness.respawn() + harness.session.rememberSyntheticKillExit(harness.id, replacement.incarnationId) + const marker = harness.session.syntheticKillExitPtyIds.get(harness.id) + harness.deliverProviderExit({ + id: harness.id, + code: 137, + incarnationId: harness.result.incarnationId + }) + expect(harness.session.syntheticKillExitPtyIds.get(harness.id)).toBe(marker) + expect(harness.runtime.captureState()).toMatchObject({ + connected: true, + incarnationId: replacement.incarnationId + }) + expect(harness.rendererExits).toHaveLength(1) + } finally { + await harness.dispose() + } + }) + + it('matches legacy exits only to legacy synthetic markers', async () => { + const harness = await startLateExitHarness() + try { + const session = harness.session + session.rememberSyntheticKillExit(harness.id) + expect(session.consumeSyntheticKillExit(harness.id, 'new-incarnation')).toBe(false) + expect(session.syntheticKillExitPtyIds.has(harness.id)).toBe(true) + expect(session.consumeSyntheticKillExit(harness.id)).toBe(true) + session.rememberSyntheticKillExit(harness.id, 'new-incarnation') + expect(session.consumeSyntheticKillExit(harness.id)).toBe(false) + expect(session.consumeSyntheticKillExit(harness.id, 'new-incarnation')).toBe(true) + expect(session.consumeSyntheticKillExit(harness.id, 'new-incarnation')).toBe(false) + } finally { + await harness.dispose() + } + }) +}) + +describe('synthetic exit and orchestration settlement', () => { + it('settles an active dispatch and its exit listener only once', async () => { + const { OrchestrationDb } = await import('../../runtime/orchestration/db') + const { createRootDispatch } = + await import('../../runtime/orchestration/db/root-dispatch-test-fixture') + const { vi } = await import('vitest') + const harness = await startLateExitHarness() + const db = new OrchestrationDb(':memory:') + try { + harness.runtime.setOrchestrationDb(db) + const handle = 'term_late_exit' + harness.runtime.registerPreAllocatedHandleForPty(harness.id, handle) + const run = db.createRun({ + objective: 'Late exit reconciliation', + coordinatorHandle: 'term_coordinator', + coordinatorPaneKey: + '99999999-9999-4999-8999-999999999999:88888888-8888-4888-8888-888888888888' + }) + const task = db.createTask({ spec: 'Test physical exit reconciliation', runId: run.id }) + createRootDispatch(db, task.id, handle) + const failDispatch = vi.spyOn(db, 'failDispatch') + const insertMessage = vi.spyOn(db, 'insertMessage') + harness.pauseStream() + harness.subprocess._simulateData(FINAL_OUTPUT) + await harness.kill() + const settledAfterSynthetic = failDispatch.mock.calls.length + const messagesAfterSynthetic = insertMessage.mock.calls.length + expect(settledAfterSynthetic).toBe(1) + harness.resumeStream() + await harness.waitForExit() + expect(failDispatch).toHaveBeenCalledTimes(settledAfterSynthetic) + expect(insertMessage).toHaveBeenCalledTimes(messagesAfterSynthetic) + expect((await harness.capture()).exitListenerCalls).toBe(1) + expect(harness.runtime.captureState().exitCause).toEqual({ kind: 'operator_close' }) + } finally { + await harness.dispose() + db.close() + } + }) +}) diff --git a/src/main/ipc/pty/delivery/exit.ts b/src/main/ipc/pty/delivery/exit.ts index 3825be51192..aa26bb914bc 100644 --- a/src/main/ipc/pty/delivery/exit.ts +++ b/src/main/ipc/pty/delivery/exit.ts @@ -9,17 +9,21 @@ import { getRendererInFlightCharsForPty } from './accounting' import { clearFlushTimerIfIdle } from './flush' import type { PtyIpcSession } from '../session' -export function rememberSyntheticKillExit(session: PtyIpcSession, id: string): void { +export function rememberSyntheticKillExit( + session: PtyIpcSession, + id: string, + incarnationId?: string +): void { const existing = session.syntheticKillExitPtyIds.get(id) if (existing) { - clearTimeout(existing) + clearTimeout(existing.cleanupTimer) } - // Why a timed window: providers may report the real exit after kill completes; skip only that late duplicate, not a future reused id forever. + // Only the same incarnation's late exit duplicates the synthetic renderer notification. const cleanupTimer = setTimeout(() => { session.syntheticKillExitPtyIds.delete(id) }, SYNTHETIC_KILL_EXIT_DUPLICATE_WINDOW_MS) cleanupTimer.unref?.() - session.syntheticKillExitPtyIds.set(id, cleanupTimer) + session.syntheticKillExitPtyIds.set(id, { cleanupTimer, incarnationId }) } export function rememberRetiredRejectedPty(session: PtyIpcSession, id: string): void { @@ -34,12 +38,16 @@ export function rememberRetiredRejectedPty(session: PtyIpcSession, id: string): session.retiredRejectedPtyIds.set(id, cleanupTimer) } -export function consumeSyntheticKillExit(session: PtyIpcSession, id: string): boolean { - const cleanupTimer = session.syntheticKillExitPtyIds.get(id) - if (!cleanupTimer) { +export function consumeSyntheticKillExit( + session: PtyIpcSession, + id: string, + incarnationId?: string +): boolean { + const pending = session.syntheticKillExitPtyIds.get(id) + if (!pending || pending.incarnationId !== incarnationId) { return false } - clearTimeout(cleanupTimer) + clearTimeout(pending.cleanupTimer) session.syntheticKillExitPtyIds.delete(id) return true } diff --git a/src/main/ipc/pty/delivery/wire-session.ts b/src/main/ipc/pty/delivery/wire-session.ts index 5425acb9f10..6e683fbbfe0 100644 --- a/src/main/ipc/pty/delivery/wire-session.ts +++ b/src/main/ipc/pty/delivery/wire-session.ts @@ -89,9 +89,11 @@ export function wirePtyIpcSession(session: PtyIpcSession): void { session.requestSerializedBuffer = (ptyId, opts) => requestSerializedBuffer(session, ptyId, opts) session.shutdownProviderAndDetectExit = (provider, id, opts) => shutdownProviderAndDetectExit(provider, id, opts) - session.rememberSyntheticKillExit = (id) => rememberSyntheticKillExit(session, id) + session.rememberSyntheticKillExit = (id, incarnationId) => + rememberSyntheticKillExit(session, id, incarnationId) session.rememberRetiredRejectedPty = (id) => rememberRetiredRejectedPty(session, id) - session.consumeSyntheticKillExit = (id) => consumeSyntheticKillExit(session, id) + session.consumeSyntheticKillExit = (id, incarnationId) => + consumeSyntheticKillExit(session, id, incarnationId) session.syncPtyBackgroundedDelivery = (id, caller) => syncPtyBackgroundedDelivery(session, id, caller) session.resyncBackgroundedDeliveriesAfterGateReset = () => diff --git a/src/main/ipc/pty/ipc/renderer-kill.ts b/src/main/ipc/pty/ipc/renderer-kill.ts index ecdde7088f9..9d8dfa8c30b 100644 --- a/src/main/ipc/pty/ipc/renderer-kill.ts +++ b/src/main/ipc/pty/ipc/renderer-kill.ts @@ -18,7 +18,7 @@ export type PtyKillIpcDeps = { id: string, opts: { immediate?: boolean; keepHistory?: boolean; deadlineMs?: number } ) => Promise - rememberSyntheticKillExit: (id: string) => void + rememberSyntheticKillExit: (id: string, incarnationId?: string) => void sendPtyExitToRenderer: (payload: { id: string; code: number; incarnationId?: string }) => void } @@ -70,7 +70,7 @@ export function installPtyKillIpcHandler(deps: PtyKillIpcDeps): void { }) runtime?.markPtyLivenessUnverifiable?.(args.id, SSH_PROVIDER_UNREGISTERED_REASON) runtime?.onPtyExit(args.id, -1, incarnationId) - rememberSyntheticKillExit(args.id) + rememberSyntheticKillExit(args.id, incarnationId) sendPtyExitToRenderer({ id: args.id, code: -1, @@ -100,7 +100,7 @@ export function installPtyKillIpcHandler(deps: PtyKillIpcDeps): void { const incarnationId = finishPtyShutdown(args.id, connectionId, store) if (!providerExitObserved) { runtime?.onPtyExit(args.id, -1, incarnationId) - rememberSyntheticKillExit(args.id) + rememberSyntheticKillExit(args.id, incarnationId) sendPtyExitToRenderer({ id: args.id, code: -1, diff --git a/src/main/ipc/pty/provider/bind-listeners.ts b/src/main/ipc/pty/provider/bind-listeners.ts index 8febd1e3534..935d7f60432 100644 --- a/src/main/ipc/pty/provider/bind-listeners.ts +++ b/src/main/ipc/pty/provider/bind-listeners.ts @@ -87,18 +87,23 @@ export function bindProviderListeners(session: PtyIpcSession): void { if (!isCurrentPtyExit(payload)) { return } - if (session.consumeSyntheticKillExit(payload.id)) { - return - } + const syntheticExit = session.consumeSyntheticKillExit(payload.id, payload.incarnationId) if (!isLocalProvider) { clearProviderPtyState(payload.id) ptyOwnership.delete(payload.id) markClaudePtyExited(payload.id) + if (syntheticExit) { + session.runtime?.markPtyStopRequested(payload.id) + } session.runtime?.onPtyExit(payload.id, payload.code, payload.incarnationId, { providerExitObserved: true, ...(payload.cause ? { cause: payload.cause } : {}) }) } + // The control reply can overtake stream data; the physical exit must retire that late output. + if (syntheticExit) { + return + } // Why not the whole payload: the exit cause is a main-process fact for the // runtime's records; the renderer's pty:exit contract stays as it was. session.sendPtyExitToRenderer({ diff --git a/src/main/ipc/pty/runtime/controller-deps.ts b/src/main/ipc/pty/runtime/controller-deps.ts index 0d388599bc9..c6231e5ef0b 100644 --- a/src/main/ipc/pty/runtime/controller-deps.ts +++ b/src/main/ipc/pty/runtime/controller-deps.ts @@ -68,7 +68,7 @@ export type PtyRuntimeControllerDeps = { id: string, opts: { immediate?: boolean; keepHistory?: boolean; deadlineMs?: number } ) => Promise - rememberSyntheticKillExit: (id: string) => void + rememberSyntheticKillExit: (id: string, incarnationId?: string) => void rememberRetiredRejectedPty: (id: string) => void sendPtyExitToRenderer: (payload: { id: string; code: number; incarnationId?: string }) => void sendPtySpawnedToRenderer: (id: string) => void diff --git a/src/main/ipc/pty/runtime/kill.ts b/src/main/ipc/pty/runtime/kill.ts index c04950c1245..98bdc1e97ae 100644 --- a/src/main/ipc/pty/runtime/kill.ts +++ b/src/main/ipc/pty/runtime/kill.ts @@ -48,7 +48,7 @@ export function killPtyFromRuntimeController( // The relay was never asked, so the remote shell is still running. Keep the order. recordUndelivered(incarnationId) runtime?.onPtyExit(ptyId, -1, incarnationId) - rememberSyntheticKillExit(ptyId) + rememberSyntheticKillExit(ptyId, incarnationId) sendPtyExitToRenderer({ id: ptyId, code: -1, @@ -66,7 +66,7 @@ export function killPtyFromRuntimeController( const incarnationId = finishPtyShutdown(ptyId, connectionId, store) if (!providerExitObserved && !retired) { runtime?.onPtyExit(ptyId, -1, incarnationId) - rememberSyntheticKillExit(ptyId) + rememberSyntheticKillExit(ptyId, incarnationId) sendPtyExitToRenderer({ id: ptyId, code: -1, @@ -80,7 +80,7 @@ export function killPtyFromRuntimeController( const incarnationId = finishPtyShutdown(ptyId, connectionId, store) if (!retired) { runtime?.onPtyExit(ptyId, -1, incarnationId) - rememberSyntheticKillExit(ptyId) + rememberSyntheticKillExit(ptyId, incarnationId) sendPtyExitToRenderer({ id: ptyId, code: -1, @@ -154,8 +154,9 @@ export function retireRejectedPtyFromRuntimeController( if (!ptyOwnership.has(ptyId)) { return } - runtime?.onPtyExit(ptyId, -1, ptyIncarnationById.get(ptyId)) - rememberSyntheticKillExit(ptyId) + const incarnationId = ptyIncarnationById.get(ptyId) + runtime?.onPtyExit(ptyId, -1, incarnationId) + rememberSyntheticKillExit(ptyId, incarnationId) sendPtyExitToRenderer({ id: ptyId, code: -1, @@ -175,7 +176,7 @@ export function retireRejectedPtyFromRuntimeController( connectionId ??= parsedSshId?.connectionId const incarnationId = finishPtyShutdown(ptyId, connectionId, store) runtime?.onPtyExit(ptyId, 0, incarnationId) - rememberSyntheticKillExit(ptyId) + rememberSyntheticKillExit(ptyId, incarnationId) sendPtyExitToRenderer({ id: ptyId, code: 0, @@ -270,7 +271,7 @@ export async function stopAndWaitPtyFromRuntimeController( // await, but the relay lease must still be tombstoned. const incarnationId = finishPtyShutdown(ptyId, connectionId, store) runtime?.onPtyExit(ptyId, -1, incarnationId) - rememberSyntheticKillExit(ptyId) + rememberSyntheticKillExit(ptyId, incarnationId) sendPtyExitToRenderer({ id: ptyId, code: -1, @@ -325,7 +326,7 @@ export async function stopAndWaitPtyFromRuntimeController( // The owning provider's fresh inventory observed absence, so this is a // death certificate even when its exit event was missed. runtime?.onPtyExit(ptyId, 0, incarnationId) - rememberSyntheticKillExit(ptyId) + rememberSyntheticKillExit(ptyId, incarnationId) sendPtyExitToRenderer({ id: ptyId, code: 0, diff --git a/src/main/ipc/pty/session.ts b/src/main/ipc/pty/session.ts index 3570e428b56..f6a09ed0fee 100644 --- a/src/main/ipc/pty/session.ts +++ b/src/main/ipc/pty/session.ts @@ -96,7 +96,10 @@ export type PtyIpcSession = { producerFlowControl: PtyProducerFlowController sourceCreditPendingPtys: Set backgroundedDeliverySyncByPty: Map - syntheticKillExitPtyIds: Map + syntheticKillExitPtyIds: Map< + string, + { cleanupTimer: NodeJS.Timeout; incarnationId: string | undefined } + > reversibleStopOwnersByPtyId: Map retiredRejectedPtyIds: Map pendingSerializeRequests: Map< @@ -156,9 +159,9 @@ export type PtyIpcSession = { id: string, opts: { immediate?: boolean; keepHistory?: boolean; deadlineMs?: number } ) => Promise - rememberSyntheticKillExit: (id: string) => void + rememberSyntheticKillExit: (id: string, incarnationId?: string) => void rememberRetiredRejectedPty: (id: string) => void - consumeSyntheticKillExit: (id: string) => boolean + consumeSyntheticKillExit: (id: string, incarnationId?: string) => boolean syncPtyBackgroundedDelivery: (id: string, caller: string) => void resyncBackgroundedDeliveriesAfterGateReset: () => void transitionHiddenRendererPtyDeliveryState: ( From c09e8fe59a19521da861c72fb98698d269c6b2e3 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:28:55 -0700 Subject: [PATCH 28/59] fix(sessions): stop transcript catch-up after TUI owner close (#21002) Co-authored-by: m4air --- docs/audits/tui-transcript-close/README.md | 30 ++++ .../audits/tui-transcript-close/reproduce.mjs | 140 +++++++++++++++ docs/audits/tui-transcript-close/results.json | 30 ++++ ...tured-agent-session-handoff-owner-close.ts | 1 + .../structured-tui-transcript-close.test.ts | 168 ++++++++++++++++++ 5 files changed, 369 insertions(+) create mode 100644 docs/audits/tui-transcript-close/README.md create mode 100644 docs/audits/tui-transcript-close/reproduce.mjs create mode 100644 docs/audits/tui-transcript-close/results.json create mode 100644 src/main/native-chat/agent-session-wire/structured-tui-transcript-close.test.ts diff --git a/docs/audits/tui-transcript-close/README.md b/docs/audits/tui-transcript-close/README.md new file mode 100644 index 00000000000..a5df3b9553d --- /dev/null +++ b/docs/audits/tui-transcript-close/README.md @@ -0,0 +1,30 @@ +# Closed TUI sessions retain transcript watchers + +After a structured session hands off to a terminal, `StructuredTuiTranscriptCatchup` tails the provider transcript. Successful `StructuredAgentSessionHost.close` stopped the TUI owner, durably released its lease, and removed the host session, but did not stop its transcript catchup. The live watcher and catchup state, including the previously seen message IDs, remained reachable. Repeated closes of distinct sessions could accumulate these resources until host teardown. + +The fix calls the existing `stopTuiHistoryCatchup` callback after the verified terminal close and durable lease transition succeed. It removes the catchup state and unsubscribes the watcher before later journal eviction. An unverified stop or failed lease write preserves the watcher for retry. A later journal-close failure cannot undo completed watcher cleanup. The execution host retains authority; no client-side inference of remote process death or wire change is involved. + +## Reproduce + +From the repository root with existing dependencies installed: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node docs/audits/tui-transcript-close/reproduce.mjs +``` + +The proof uses the actual structured host, record store, journal, handoff coordinator, and transcript watcher against a temporary synthetic Codex transcript. Only provider process acquisition/stop is a test transport; no real shell or Orca window launches. It removes the single cleanup call in a temporary Vite transform for the baseline, then runs the same four tests against the fixed source. It uses the repository's `runProcess` and cleans temporary configurations/module files. Source hashes are recorded in `results.json`. + +| Version | Passed | Failed | +| ---------- | -----: | -----: | +| Before fix | 0 | 4 | +| With fix | 4 | 0 | + +The successful-close case observes one added watcher, proves live TUI text reaches the journal, closes the session, verifies the lease is released and session removed, and expects the watcher count to return to its original value. Before the fix it stays one higher. The remaining cases exercise unverified terminal stop/retry, failed durable transition/retry, and failed journal eviction/retry. These are four checks of one cleanup omission. + +Existing catchup tests also verify that live appends and recovery of writes made while the host was down remain intact. All seven targeted tests passed, as did the Node typecheck and direct lint. + +## Version and limits + +Named-path reads of `v1.4.198` confirm that its host close calls the same owner-close helper, that helper omits catchup cleanup, and its catchup owns the same state/watcher lifetime. The executable comparison uses current production source. This establishes a retaining path present in the reported build; it does not establish that either #19831 or #19768 exercised this handoff-and-close sequence, or measure either report's memory growth. + +The separate asynchronous acquisition race remains open: teardown calls `stopAll` before draining handoffs, while catchup setup can still be awaiting path resolution or subscription acquisition. Merely rejecting a canceled preparation is insufficient as a full teardown fix: the forward handoff's existing failure recovery may acquire a native replacement, and the handoff drain has a five-second limit. That race needs its own owner-cancellation policy and regression proof; this change covers successful close of an acquired TUI owner. diff --git a/docs/audits/tui-transcript-close/reproduce.mjs b/docs/audits/tui-transcript-close/reproduce.mjs new file mode 100644 index 00000000000..23d3bcb34a4 --- /dev/null +++ b/docs/audits/tui-transcript-close/reproduce.mjs @@ -0,0 +1,140 @@ +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/native-chat/agent-session-wire/structured-agent-session-handoff-owner-close.ts' +const testPath = 'src/main/native-chat/agent-session-wire/structured-tui-transcript-close.test.ts' +const absolute = resolve(root, productionPath) +const current = await readFile(absolute, 'utf8') +const cleanup = ' input.deps.stopTuiHistoryCatchup?.(input.sessionId)\n' +if (current.split(cleanup).length !== 2) { + throw new Error('Expected exactly one successful-close cleanup; review the proof transform.') +} +const baseline = current.replace(cleanup, '') +const beforeSources = { [absolute.replaceAll('\\', '/')]: baseline } +const sourceHashes = { + [productionPath]: { + before: createHash('sha256').update(baseline).digest('hex'), + after: createHash('sha256').update(current).digest('hex') + }, + [testPath]: { + current: createHash('sha256') + .update(await readFile(resolve(root, testPath))) + .digest('hex') + } +} + +const scratch = await mkdtemp(join(tmpdir(), 'orca-tui-transcript-close-')) +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 baselineConfig = join(scratch, 'before.config.mjs') + const fixedConfig = join(scratch, 'after.config.mjs') + const includes = [testPath] + const configImport = JSON.stringify(pathToFileURL(resolve(root, 'config/vitest.config.ts')).href) + await writeFile( + baselineConfig, + `import base from ${configImport}; +const beforeSources = ${JSON.stringify(beforeSources)}; +export default {...base, test: {...base.test, include: ${JSON.stringify(includes)}}, plugins: [{ + name: 'tui-transcript-close-before-fix', enforce: 'pre', + transform(_code, id) { + const before = beforeSources[id.replaceAll('\\\\', '/').split('?')[0]]; + return before === undefined ? null : {code: before, map: null}; + } +}]};\n` + ) + + await writeFile( + fixedConfig, + `import base from ${configImport};\nexport default {...base, test: {...base.test, include: ${JSON.stringify(includes)}}};\n` + ) + + async function run(label, config) { + const report = join(scratch, `${label}.json`) + 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, + timeoutMs: 90_000, + maxOutputBytes: 4 * 1024 * 1024 + }) + let parsed + try { + parsed = JSON.parse(await readFile(report, 'utf8')) + } catch (error) { + throw new Error(`${label} runner failed: ${result.stderr || result.stdout}`, { cause: error }) + } + return { + exitCode: result.code, + passed: parsed.numPassedTests, + failed: parsed.numFailedTests, + failedCases: parsed.testResults.flatMap((suite) => + suite.assertionResults + .filter((test) => test.status === 'failed') + .map((test) => test.fullName) + ) + } + } + + const before = await run('before', baselineConfig) + const after = await run('after', fixedConfig) + const passed = + before.failed === 4 && + before.passed === 0 && + before.passed + before.failed === 4 && + after.passed === 4 && + after.failed === 0 + console.log( + JSON.stringify( + { + comparison: + 'Actual structured host/store/journal/transcript watcher; baseline removes only the successful-close stop callback in a temporary Vite transform', + 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 }) +} diff --git a/docs/audits/tui-transcript-close/results.json b/docs/audits/tui-transcript-close/results.json new file mode 100644 index 00000000000..67c80b1abbe --- /dev/null +++ b/docs/audits/tui-transcript-close/results.json @@ -0,0 +1,30 @@ +{ + "comparison": "Actual structured host/store/journal/transcript watcher; baseline removes only the successful-close stop callback in a temporary Vite transform", + "sourceHashes": { + "src/main/native-chat/agent-session-wire/structured-agent-session-handoff-owner-close.ts": { + "before": "a8f6d3a932c50784e9f614276989e4ef84b85eed0df4a2e0877df28e8d4640e5", + "after": "1cb420c0c27248eea74e07b6ce84530036fbabe656c360e814f9d937a35ba0d7" + }, + "src/main/native-chat/agent-session-wire/structured-tui-transcript-close.test.ts": { + "current": "bd3109b31f01bf84566ce26d1fc2c6d2cd9c7bbb64e0d95e445a0515ed32cf9f" + } + }, + "before": { + "exitCode": 1, + "passed": 0, + "failed": 4, + "failedCases": [ + "retires the transcript watcher when a live TUI session closes", + "keeps live history when terminal stop is unverified and retires it on retry", + "keeps the watcher until the durable owner transition succeeds", + "keeps transcript cleanup complete when later journal eviction needs retry" + ] + }, + "after": { + "exitCode": 0, + "passed": 4, + "failed": 0, + "failedCases": [] + }, + "passed": true +} diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-handoff-owner-close.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-handoff-owner-close.ts index 634eab51423..9eb394734eb 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-handoff-owner-close.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-handoff-owner-close.ts @@ -30,6 +30,7 @@ export async function closeRetainedTuiOwner(input: { journalSettlement: 'not-required' }) ) + input.deps.stopTuiHistoryCatchup?.(input.sessionId) input.releaseOwner(input.sessionId) return true } diff --git a/src/main/native-chat/agent-session-wire/structured-tui-transcript-close.test.ts b/src/main/native-chat/agent-session-wire/structured-tui-transcript-close.test.ts new file mode 100644 index 00000000000..fa29002c934 --- /dev/null +++ b/src/main/native-chat/agent-session-wire/structured-tui-transcript-close.test.ts @@ -0,0 +1,168 @@ +import { appendFile, mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { beforeEach, expect, it, vi } from 'vitest' +import { getActiveNativeChatWatcherCount } from '../transcript-watcher-count' +import { + CALLER, + adapter, + hostTestState, + replaceHostTestState +} from './structured-agent-session-host-test-harness' +import { + HOST_TEST_NOW as NOW, + HOST_TEST_SESSION as SESSION, + HOST_TEST_THREAD as THREAD, + hostTestAttachParams, + hostTestOperationId +} from './structured-agent-session-host-test-data' +import { StructuredAgentSessionHost } from './structured-agent-session-host' +import { StructuredHandoffTestRequests } from './structured-agent-session-handoff-test-requests' +import type { + StructuredAgentSessionHandoffTransport, + StructuredTuiOwner +} from './structured-agent-session-handoff-types' + +let host: StructuredAgentSessionHost +let rollout: string +let watcherBaseline: number +let closeTuiOwner: ReturnType< + typeof vi.fn> +> + +function rolloutLine(message: string): string { + return `${JSON.stringify({ + type: 'event_msg', + timestamp: '2026-08-11T10:00:00.000Z', + payload: { type: 'agent_message', message } + })}\n` +} + +function tuiOwner(fence: number, spawnToken: string): StructuredTuiOwner { + return { + terminal: { handle: 'term-tui', tabId: 'tab-tui', paneKey: 'pane-tui', ptyId: 'pty-tui' }, + process: { hostId: 'local', pid: 5200, processStartTimeMs: NOW, spawnToken }, + link: { + linkId: `tui-link-${fence}`, + handle: { provider: 'codex', threadId: THREAD }, + origin: 'resumed', + mintedAtFence: fence, + observedAt: NOW + } + } +} + +async function expectTranscriptMessage(text: string): Promise { + await appendFile(rollout, rolloutLine(text)) + await vi.waitFor(() => { + const history = host.history({ sessionId: SESSION, direction: 'tail' }) + expect( + history.ok && + history.page.items.some( + (item) => + item.body.kind === 'message' && + item.body.blocks.some((block) => block.type === 'text' && block.text === text) + ) + ).toBe(true) + }) +} + +beforeEach(async () => { + const initial = hostTestState() + await initial.host.flushAllStreamedEvents() + watcherBaseline = getActiveNativeChatWatcherCount() + closeTuiOwner = vi.fn(async () => ({})) + host = new StructuredAgentSessionHost({ + ...initial.host.deps, + adapter: { ...adapter(), closeSession: vi.fn(async () => true) }, + handoffTransport: { + hostLabel: 'Test host', + launchTui: async ({ fence, spawnToken }) => tuiOwner(fence, spawnToken), + reproveTuiOwner: async ({ owner }) => owner, + recoverTuiOwner: async (record) => + tuiOwner(record.lease.runtimeFence, record.lease.reservedSpawnToken ?? 'recovered'), + stopRecoveredOwner: async () => undefined, + closeTuiOwner, + waitForTuiExit: async () => ({}), + waitForTuiIdleOrExit: async () => 'idle', + tuiStatus: () => 'idle' + } + }) + replaceHostTestState({ host, store: initial.store }) + const accountHome = join(initial.root, 'codex-home') + const sessionsDir = join(accountHome, 'sessions', '2026', '08', '11') + await mkdir(sessionsDir, { recursive: true }) + rollout = join(sessionsDir, `rollout-2026-08-11T10-00-00-${THREAD}.jsonl`) + await writeFile(rollout, rolloutLine('before handoff')) + expect( + await host.attach( + CALLER, + hostTestAttachParams(null, { accountHome: { variable: 'CODEX_HOME', path: accountHome } }) + ) + ).toMatchObject({ ok: true }) + const requests = new StructuredHandoffTestRequests( + NOW, + SESSION, + () => initial.store.getRecord(SESSION)?.lease.runtimeFence ?? 0 + ) + expect( + await host.requestHandoff( + CALLER, + requests.request('to-tui', 'now', { operationId: hostTestOperationId() }) + ) + ).toMatchObject({ ok: true }) + await host['handoffs'].drain() + expect(await host.handoffStatus(SESSION)).toMatchObject({ owner: 'tui', phase: 'idle' }) + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline + 1) +}) + +it('retires the transcript watcher when a live TUI session closes', async () => { + await expectTranscriptMessage('while TUI live') + await host.close(SESSION) + expect(host.hasSession(SESSION)).toBe(false) + expect(hostTestState().store.getRecord(SESSION)?.lease.claimStatus).toBe('released') + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline) + await appendFile(rollout, rolloutLine('after TUI close')) + await host.close(SESSION) + expect(closeTuiOwner).toHaveBeenCalledOnce() + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline) +}) + +it('keeps live history when terminal stop is unverified and retires it on retry', async () => { + closeTuiOwner.mockRejectedValueOnce(new Error('terminal exit unverified')) + await expect(host.close(SESSION)).rejects.toThrow('terminal exit unverified') + expect(host.hasSession(SESSION)).toBe(true) + expect(hostTestState().store.getRecord(SESSION)?.lease.claimStatus).toBe('live') + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline + 1) + await expectTranscriptMessage('after unverified stop') + await host.close(SESSION) + expect(closeTuiOwner).toHaveBeenCalledTimes(2) + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline) +}) + +it('keeps the watcher until the durable owner transition succeeds', async () => { + vi.spyOn(hostTestState().store, 'transitionHandoff').mockRejectedValueOnce( + new Error('lease write failed') + ) + await expect(host.close(SESSION)).rejects.toThrow('lease write failed') + expect(host.hasSession(SESSION)).toBe(true) + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline + 1) + await host.close(SESSION) + expect(closeTuiOwner).toHaveBeenCalledTimes(2) + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline) +}) + +it('keeps transcript cleanup complete when later journal eviction needs retry', async () => { + const session = host['sessions'].get(SESSION) + expect(session).toBeDefined() + if (!session) { + throw new Error('TUI session missing') + } + vi.spyOn(session.journal, 'close').mockRejectedValueOnce(new Error('journal close failed')) + await expect(host.close(SESSION)).rejects.toThrow('forget-session') + expect(host.hasSession(SESSION)).toBe(true) + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline) + await host.close(SESSION) + expect(host.hasSession(SESSION)).toBe(false) + expect(closeTuiOwner).toHaveBeenCalledOnce() + expect(getActiveNativeChatWatcherCount()).toBe(watcherBaseline) +}) From 0e935c4b0ac132b77050fa918d0598fd78f51161 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:28:58 -0700 Subject: [PATCH 29/59] fix(runtime): terminate nonblank tail scan at the first row (#21018) Co-authored-by: m4air --- .../terminal-wait-leading-blank/README.md | 23 ++ .../terminal-wait-leading-blank/reproduce.mjs | 249 ++++++++++++++++++ .../terminal-wait-leading-blank/results.json | 172 ++++++++++++ .../runtime/terminal-wait-tail-window.test.ts | 77 ++++++ src/main/runtime/terminal-wait-tail-window.ts | 3 +- 5 files changed, 523 insertions(+), 1 deletion(-) create mode 100644 docs/audits/terminal-wait-leading-blank/README.md create mode 100644 docs/audits/terminal-wait-leading-blank/reproduce.mjs create mode 100644 docs/audits/terminal-wait-leading-blank/results.json create mode 100644 src/main/runtime/terminal-wait-tail-window.test.ts diff --git a/docs/audits/terminal-wait-leading-blank/README.md b/docs/audits/terminal-wait-leading-blank/README.md new file mode 100644 index 00000000000..959fde7c9af --- /dev/null +++ b/docs/audits/terminal-wait-leading-blank/README.md @@ -0,0 +1,23 @@ +# Terminal wait tail-window termination + +`startOfLastNonBlankLines` loops indefinitely if its input begins with a newline and contains fewer nonblank rows than requested. Once the backward cursor reaches zero, JavaScript `lastIndexOf` clamps its negative start position to zero and rediscovers the same first newline. The cursor stops advancing. + +The fix ends the scan when the cursor reaches zero and returns the existing short-tail offset, zero. It changes no prompt patterns or readiness rules. The ordinary finite-window selection cases retain their previous offsets. + +## Reproduce + +```sh +ORCA_BACKGROUND_LAUNCH=1 node docs/audits/terminal-wait-leading-blank/reproduce.mjs > /tmp/orca-terminal-wait-leading-blank.json +``` + +The script bundles actual source and reverses only the two-line termination change for the baseline. Each case runs in an isolated child with a two-second deadline and 128 MiB heap limit. The parent confirms child termination; no app windows open. Five direct helper/detector cases time out before and return after. A sufficient-row control and actual headless terminal projection controls pass in both variants. Results include source hashes, platform, timing, and an explicit v1.4.198 helper comparison. + +The regression suite uses a separate child for inputs that could hang the worker. It also tests exact row offsets with intervening whitespace, trailing blanks and tails shorter than the requested window. Fifty helper/detector tests pass. + +## Production and incident limits + +The helper is byte-identical in v1.4.198. However, all inspected main/provider/renderer visible-screen projection routes pass through `visibleNonBlankTerminalLines`, and ordinary retained-tail construction removes blank rows too. The real headless producer control confirms this filtering. Calling the public detector directly with a leading newline is therefore insufficient evidence that those production routes trigger the defect. + +A clipped 300-character preview can start at a newline. The preview fallback also preserves it when passed empty retained rows; the proof records both facts. It does not establish an actual application lifecycle that combines that fallback with a live detector call. That remains unproven. + +This is a defensive termination fix found during the memory audit. The loop itself does not allocate a growing collection. No memory magnitude was measured, and it is not an attribution of #19768's main-process growth or #19831's application-scope OOM. diff --git a/docs/audits/terminal-wait-leading-blank/reproduce.mjs b/docs/audits/terminal-wait-leading-blank/reproduce.mjs new file mode 100644 index 00000000000..6ee47701120 --- /dev/null +++ b/docs/audits/terminal-wait-leading-blank/reproduce.mjs @@ -0,0 +1,249 @@ +import { createHash } from 'node:crypto' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { isDeepStrictEqual } from 'node:util' +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 sourcePath = 'src/main/runtime/terminal-wait-tail-window.ts' +const absoluteSource = resolve(root, sourcePath) +const current = await readFile(absoluteSource, 'utf8') +const loop = ' while (lineEnd > 0) {' +const end = ' lineEnd = lineStart - 1\n }\n return 0\n}' +if (current.split(loop).length !== 2 || current.split(end).length !== 2) { + throw new Error('Source changed; review the baseline transform.') +} +const baseline = current + .replace(loop, ' for (;;) {') + .replace(end, ' lineEnd = lineStart - 1\n }\n}') +const sha256 = (value) => createHash('sha256').update(value).digest('hex') +const supportingSources = [ + 'src/main/runtime/terminal-wait-detection.ts', + 'src/main/runtime/orca-runtime-terminal-projection.ts', + 'src/main/runtime/terminal-tail-read.ts', + 'src/main/runtime/terminal-tail-state.ts', + 'src/main/runtime/terminal-wait-tail-state.ts', + 'src/main/daemon/headless-emulator.ts', + 'src/main/runtime/terminal-wait-tail-window.test.ts' +] +const supportingSourceHashes = Object.fromEntries( + await Promise.all( + supportingSources.map(async (path) => [path, sha256(await readFile(resolve(root, path)))]) + ) +) +const scratch = await mkdtemp(join(tmpdir(), 'orca-terminal-wait-blank-')) +const require = createRequire(import.meta.url) +let runnerId + +try { + const runnerPath = join(scratch, 'run-process.cjs') + await build({ + entryPoints: [resolve(root, 'src/shared/child-process/run-process.ts')], + outfile: runnerPath, + bundle: true, + platform: 'node', + format: 'cjs', + logLevel: 'silent' + }) + runnerId = require.resolve(runnerPath) + const { runProcess } = require(runnerId) + const entry = ` + import { startOfLastNonBlankLines } from './src/main/runtime/terminal-wait-tail-window'; + import { detectTerminalWaitBlockedReason, isKnownReadyPromptPreview } from './src/main/runtime/terminal-wait-detection'; + import { HeadlessEmulator } from './src/main/daemon/headless-emulator'; + import { projectTerminalVisibleLines, projectTerminalTailLines } from './src/main/runtime/orca-runtime-terminal-projection'; + import { buildPreview } from './src/main/runtime/terminal-tail-state'; + import { buildTerminalWaitText } from './src/main/runtime/terminal-wait-tail-state'; + const input = JSON.parse(process.argv[2]); + async function main() { + process.stdout.write(JSON.stringify({ phase: 'entered', mode: input.mode }) + '\\n'); + let value; + if (input.mode === 'window') value = startOfLastNonBlankLines(input.text, input.count); + if (input.mode === 'blocked') value = detectTerminalWaitBlockedReason(input.text); + if (input.mode === 'ready') value = isKnownReadyPromptPreview(input.text); + if (input.mode === 'producer-controls') { + const emulator = new HeadlessEmulator({ cols: 80, rows: 12, scrollback: 0 }); + try { + await emulator.write('\\r\\nordinary output\\r\\n'); + const raw = emulator.getVisibleLines(); + const visible = projectTerminalVisibleLines(emulator).lines; + const tail = projectTerminalTailLines(emulator, 12).lines; + const longLines = ['prefix', 'x'.repeat(299)]; + const preview = buildPreview(longLines, ''); + const waitText = buildTerminalWaitText(longLines, '', preview); + value = { + rawRowsStartBlank: raw[0] === '', + visibleRows: visible, + projectedTail: tail, + visibleClassification: detectTerminalWaitBlockedReason(visible.join('\\n')), + ordinaryTailClassification: detectTerminalWaitBlockedReason(buildTerminalWaitText(raw, '', '')), + clippedPreviewStartsNewline: preview.startsWith('\\n'), + retainedTailStartsNewline: waitText.startsWith('\\n'), + retainedTailClassification: detectTerminalWaitBlockedReason(waitText), + emptyTailFallbackStartsNewline: buildTerminalWaitText([], '', preview).startsWith('\\n') + }; + } finally { emulator.dispose(); } + } + process.stdout.write(JSON.stringify({ phase: 'returned', value }) + '\\n'); + } + main().catch(error => { process.stderr.write(String(error)); process.exitCode = 1; }); + ` + const cases = [ + { name: 'leading newline only', mode: 'window', text: '\n', count: 12, expected: 0 }, + { name: 'leading newline and text', mode: 'window', text: '\ntext', count: 12, expected: 0 }, + { name: 'blank screen classification', mode: 'blocked', text: '\n\n', expected: null }, + { + name: 'leading blank trust dialog', + mode: 'blocked', + text: '\nDo you trust this workspace directory?\n1. Yes\n2. No', + expected: 'agent-trust-workspace' + }, + { + name: 'leading blank ready header', + mode: 'ready', + text: '\nOpenAI Codex\nmodel: test\ndirectory: /workspace', + expected: true + }, + { + name: 'enough nonblank rows', + mode: 'window', + text: '\nfirst\nsecond', + count: 1, + expected: 7 + }, + { + name: 'production producer controls', + mode: 'producer-controls', + expected: { + rawRowsStartBlank: true, + visibleRows: ['ordinary output'], + projectedTail: ['ordinary output'], + visibleClassification: null, + ordinaryTailClassification: null, + clippedPreviewStartsNewline: true, + retainedTailStartsNewline: false, + retainedTailClassification: null, + emptyTailFallbackStartsNewline: true + } + } + ] + const results = {} + for (const [label, source] of Object.entries({ before: baseline, after: current })) { + const childPath = join(scratch, `${label}.cjs`) + await build({ + stdin: { contents: entry, resolveDir: root }, + outfile: childPath, + bundle: true, + platform: 'node', + format: 'cjs', + logLevel: 'silent', + plugins: [ + { + name: 'terminal-wait-baseline', + setup(builder) { + builder.onLoad({ filter: /terminal-wait-tail-window\.ts$/ }, (args) => + resolve(args.path) === absoluteSource ? { contents: source, loader: 'ts' } : null + ) + } + } + ] + }) + results[label] = [] + for (const input of cases) { + let childTerminated = false + const started = performance.now() + const result = await runProcess({ + program: process.execPath, + args: ['--max-old-space-size=128', childPath, JSON.stringify(input)], + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs: 2_000, + maxOutputBytes: 8192, + onChildTerminated: () => { + childTerminated = true + } + }) + const output = result.stdout + .trim() + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line)) + const expectedTimeout = label === 'before' && cases.indexOf(input) < 5 + const returned = output.find((event) => event.phase === 'returned') + if ( + !childTerminated || + result.timedOut !== expectedTimeout || + !output.some((event) => event.phase === 'entered') + ) { + throw new Error(`Unexpected ${label} result for ${input.name}: ${JSON.stringify(result)}`) + } + if ( + !expectedTimeout && + (result.code !== 0 || + !returned || + ('expected' in input && !isDeepStrictEqual(returned.value, input.expected))) + ) { + throw new Error( + `Unexpected ${label} output for ${input.name}: ${result.stdout} ${result.stderr}` + ) + } + results[label].push({ + name: input.name, + timedOut: result.timedOut, + childTerminated, + code: result.code, + signal: result.signal, + elapsedMs: Math.round(performance.now() - started), + ...(returned ? { value: returned.value } : {}) + }) + } + } + const tag = await runProcess({ + program: 'git', + args: ['show', `v1.4.198:${sourcePath}`], + cwd: root, + maxOutputBytes: 16_384 + }) + const provenance = await runProcess({ + program: 'git', + args: ['rev-parse', 'HEAD'], + cwd: root, + maxOutputBytes: 1024 + }) + process.stdout.write( + `${JSON.stringify( + { + node: process.version, + platform: process.platform, + architecture: process.arch, + revision: provenance.stdout.trim(), + source: sourcePath, + hashes: { + before: sha256(baseline), + after: sha256(current), + reportedVersion: tag.code === 0 ? sha256(tag.stdout) : null + }, + supportingSourceHashes, + reportedVersionSourceMatchesBaseline: tag.code === 0 && tag.stdout === baseline, + childTimeoutMs: 2000, + childHeapLimitMiB: 128, + scope: + 'Helper termination and producer controls; no incident attribution or retained-byte claim.', + results + }, + null, + 2 + )}\n` + ) +} finally { + if (runnerId) { + delete require.cache[runnerId] + } + await rm(scratch, { recursive: true, force: true }) +} diff --git a/docs/audits/terminal-wait-leading-blank/results.json b/docs/audits/terminal-wait-leading-blank/results.json new file mode 100644 index 00000000000..98776e8b5b8 --- /dev/null +++ b/docs/audits/terminal-wait-leading-blank/results.json @@ -0,0 +1,172 @@ +{ + "node": "v26.6.0", + "platform": "darwin", + "architecture": "arm64", + "revision": "9558152a04c499c666192dd08906be8ada09e1dc", + "source": "src/main/runtime/terminal-wait-tail-window.ts", + "hashes": { + "before": "e6bc10e924d45bf4bfbd6fabf4404f160107cf7235b11c4e140cd6c78cff9146", + "after": "8df53ea8f92461549d727f251dae7cb4415d4208228ec5f60df9dcf13b863b86", + "reportedVersion": "e6bc10e924d45bf4bfbd6fabf4404f160107cf7235b11c4e140cd6c78cff9146" + }, + "supportingSourceHashes": { + "src/main/runtime/terminal-wait-detection.ts": "226d09d2d8f7f1d692fb8ba1ed340818e6bd402b74d7bd98d86b1868a09503f0", + "src/main/runtime/orca-runtime-terminal-projection.ts": "70b815cf26864719b30b64845c0035093c16b9d85cb4bb8d25c4ae5fa63c3026", + "src/main/runtime/terminal-tail-read.ts": "3517bb22b9bf4bdf2f3acee8ceac9ca71221964cfa3f16be1f7b2ad22212b476", + "src/main/runtime/terminal-tail-state.ts": "a6f41a683d5f03023e4f6d20d653ebf069b036f82100c908a775a1890c49b2ef", + "src/main/runtime/terminal-wait-tail-state.ts": "61acc15fb8b9ce7faa0a2df80a7b6bae09dfed103c15b9c6c35a66d6ad585db1", + "src/main/daemon/headless-emulator.ts": "ace96102285c3967cd9544e7696fe73e1ac6df576b476939aa9819f5c298bab6", + "src/main/runtime/terminal-wait-tail-window.test.ts": "0faab1ca7b01e43ab555fd7fe2979975b818c22f25eca4f74dea3eb04f02c66b" + }, + "reportedVersionSourceMatchesBaseline": true, + "childTimeoutMs": 2000, + "childHeapLimitMiB": 128, + "scope": "Helper termination and producer controls; no incident attribution or retained-byte claim.", + "results": { + "before": [ + { + "name": "leading newline only", + "timedOut": true, + "childTerminated": true, + "code": null, + "signal": "SIGTERM", + "elapsedMs": 2004 + }, + { + "name": "leading newline and text", + "timedOut": true, + "childTerminated": true, + "code": null, + "signal": "SIGTERM", + "elapsedMs": 2002 + }, + { + "name": "blank screen classification", + "timedOut": true, + "childTerminated": true, + "code": null, + "signal": "SIGTERM", + "elapsedMs": 2002 + }, + { + "name": "leading blank trust dialog", + "timedOut": true, + "childTerminated": true, + "code": null, + "signal": "SIGTERM", + "elapsedMs": 2002 + }, + { + "name": "leading blank ready header", + "timedOut": true, + "childTerminated": true, + "code": null, + "signal": "SIGTERM", + "elapsedMs": 2002 + }, + { + "name": "enough nonblank rows", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 32, + "value": 7 + }, + { + "name": "production producer controls", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 36, + "value": { + "rawRowsStartBlank": true, + "visibleRows": ["ordinary output"], + "projectedTail": ["ordinary output"], + "visibleClassification": null, + "ordinaryTailClassification": null, + "clippedPreviewStartsNewline": true, + "retainedTailStartsNewline": false, + "retainedTailClassification": null, + "emptyTailFallbackStartsNewline": true + } + } + ], + "after": [ + { + "name": "leading newline only", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 34, + "value": 0 + }, + { + "name": "leading newline and text", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 33, + "value": 0 + }, + { + "name": "blank screen classification", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 33, + "value": null + }, + { + "name": "leading blank trust dialog", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 33, + "value": "agent-trust-workspace" + }, + { + "name": "leading blank ready header", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 33, + "value": true + }, + { + "name": "enough nonblank rows", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 32, + "value": 7 + }, + { + "name": "production producer controls", + "timedOut": false, + "childTerminated": true, + "code": 0, + "signal": null, + "elapsedMs": 37, + "value": { + "rawRowsStartBlank": true, + "visibleRows": ["ordinary output"], + "projectedTail": ["ordinary output"], + "visibleClassification": null, + "ordinaryTailClassification": null, + "clippedPreviewStartsNewline": true, + "retainedTailStartsNewline": false, + "retainedTailClassification": null, + "emptyTailFallbackStartsNewline": true + } + } + ] + } +} diff --git a/src/main/runtime/terminal-wait-tail-window.test.ts b/src/main/runtime/terminal-wait-tail-window.test.ts new file mode 100644 index 00000000000..22a71140ee8 --- /dev/null +++ b/src/main/runtime/terminal-wait-tail-window.test.ts @@ -0,0 +1,77 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { build } from 'esbuild' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { runProcess } from '../../shared/child-process/run-process' +import { startOfLastNonBlankLines } from './terminal-wait-tail-window' + +let scratch = '' +let childPath = '' + +beforeAll(async () => { + scratch = await mkdtemp(join(tmpdir(), 'orca-tail-window-')) + childPath = join(scratch, 'leading-blank.cjs') + await build({ + stdin: { + contents: ` + import { startOfLastNonBlankLines } from './src/main/runtime/terminal-wait-tail-window'; + import { detectTerminalWaitBlockedReason, isKnownReadyPromptPreview } from './src/main/runtime/terminal-wait-detection'; + const tails = ['', '\\n', '\\n\\n', '\\ntext', '\\ntext\\n', '\\n \\t\\ntext\\n\\n']; + process.stdout.write(JSON.stringify({ + offsets: tails.map(value => startOfLastNonBlankLines(value, 12)), + blank: detectTerminalWaitBlockedReason('\\n\\n'), + ordinary: detectTerminalWaitBlockedReason('\\nordinary output'), + blocked: detectTerminalWaitBlockedReason('\\nDo you trust this workspace directory?\\n1. Yes\\n2. No'), + ready: isKnownReadyPromptPreview('\\nOpenAI Codex\\nmodel: test\\ndirectory: /workspace') + })); + `, + resolveDir: process.cwd() + }, + outfile: childPath, + bundle: true, + platform: 'node', + format: 'cjs', + logLevel: 'silent' + }) +}) + +afterAll(async () => { + if (scratch) { + await rm(scratch, { recursive: true, force: true }) + } +}) + +describe('terminal wait nonblank tail window', () => { + it('terminates on leading blank rows before classifying the remaining screen', async () => { + // Isolate the synchronous regression so its timeout cannot block the test worker. + const result = await runProcess({ + program: process.execPath, + args: ['--max-old-space-size=64', childPath], + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs: 2_000, + maxOutputBytes: 4096 + }) + expect(result.timedOut).toBe(false) + expect(result.code, result.stderr).toBe(0) + expect(JSON.parse(result.stdout)).toEqual({ + offsets: [0, 0, 0, 0, 0, 0], + blank: null, + ordinary: null, + blocked: 'agent-trust-workspace', + ready: true + }) + }) + + it.each([ + { value: 'first\nsecond\nthird', count: 2, expected: 'second\nthird' }, + { value: 'first\n\n \t\nsecond\nthird\n', count: 2, expected: 'second\nthird\n' }, + { value: '\nfirst\nsecond', count: 1, expected: 'second' }, + { value: '\nfirst\nsecond', count: 2, expected: 'first\nsecond' }, + { value: 'first\nsecond', count: 3, expected: 'first\nsecond' }, + { value: 'first\nsecond\n\n', count: 1, expected: 'second\n\n' }, + { value: ' \t\r\nsecond', count: 2, expected: ' \t\r\nsecond' } + ])('selects the last $count nonblank rows of $value', ({ value, count, expected }) => { + expect(value.slice(startOfLastNonBlankLines(value, count))).toBe(expected) + }) +}) diff --git a/src/main/runtime/terminal-wait-tail-window.ts b/src/main/runtime/terminal-wait-tail-window.ts index 788000328f1..fec141f7cde 100644 --- a/src/main/runtime/terminal-wait-tail-window.ts +++ b/src/main/runtime/terminal-wait-tail-window.ts @@ -23,7 +23,7 @@ export function startOfLastLines(value: string, count: number): number { export function startOfLastNonBlankLines(value: string, count: number): number { let seen = 0 let lineEnd = value.length - for (;;) { + while (lineEnd > 0) { const lineStart = value.lastIndexOf('\n', lineEnd - 1) + 1 if (hasNonWhitespaceBetween(value, lineStart, lineEnd)) { seen += 1 @@ -36,6 +36,7 @@ export function startOfLastNonBlankLines(value: string, count: number): number { } lineEnd = lineStart - 1 } + return 0 } function hasNonWhitespaceBetween(value: string, start: number, end: number): boolean { From 5723c5baa9a4292d2b9ea86ea38ec6ffc91e38a1 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:31:34 -0700 Subject: [PATCH 30/59] fix(runtime): preserve exited PTY authority across queued graphs (#21011) * fix(pty): reconcile daemon exits after synthetic notifications * fix(runtime): preserve exited PTY authority across queued graphs * test(runtime): include shared socket fixture for graph reproduction * docs(memory): clarify graph reproduction dependency and source hashes --------- Co-authored-by: m4air --- .../queued-terminal-graph-exit/README.md | 58 + .../queued-terminal-graph-exit/fixture.ts | 231 +++ .../preserved-history-fixture.ts | 180 ++ .../queued-terminal-graph-exit/reproduce.mjs | 174 ++ .../queued-terminal-graph-exit/results.json | 1763 +++++++++++++++++ ...-runtime-mark-pty-liveness-unverifiable.ts | 4 + src/main/runtime/orca-runtime-on-pty-exit.ts | 10 +- .../runtime/orca-runtime-sync-window-graph.ts | 8 +- .../queued-terminal-graph-exit.test.ts | 251 +++ 9 files changed, 2670 insertions(+), 9 deletions(-) create mode 100644 docs/audits/queued-terminal-graph-exit/README.md create mode 100644 docs/audits/queued-terminal-graph-exit/fixture.ts create mode 100644 docs/audits/queued-terminal-graph-exit/preserved-history-fixture.ts create mode 100644 docs/audits/queued-terminal-graph-exit/reproduce.mjs create mode 100644 docs/audits/queued-terminal-graph-exit/results.json create mode 100644 src/main/runtime/queued-terminal-graph-exit.test.ts diff --git a/docs/audits/queued-terminal-graph-exit/README.md b/docs/audits/queued-terminal-graph-exit/README.md new file mode 100644 index 00000000000..ae5c6684b39 --- /dev/null +++ b/docs/audits/queued-terminal-graph-exit/README.md @@ -0,0 +1,58 @@ +# Queued renderer graph restores an exited pane owner + +This is a separate source-level explanation for part of [#19018](https://github.com/stablyai/orca/issues/19018). It reproduces an execution host certifying exit, followed by a queued renderer graph restoring that PTY's runtime `connected` flag and making the actual stable-pane resolver throw `terminal_pane_owner_conflict` against the successor's durable binding. + +## Run + +From the checkout, with installed dependencies: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node docs/audits/queued-terminal-graph-exit/reproduce.mjs /tmp/queued-terminal-graph-exit.json +``` + +The script uses the real renderer graph publisher, main `Store.persistPtyBinding`, runtime, daemon server, adapter, and local sockets. Only the subprocess and the IPC dispatch boundary are controlled. It creates temporary data/socket paths, runs hidden Node tests, and removes its scratch files. It does not launch an Electron window or install dependencies. The JSON records source hashes and excludes randomly allocated terminal handles/incarnations. + +## Ordering + +1. Publish the mounted predecessor's graph normally. +2. Capture its next unchanged publication at the IPC dispatch boundary. The real publisher sends the mounted leaf, `mobileSessionTabs: []`, and `unchangedMobileSessionWorktrees`. +3. While that publication is queued, spawn a successor and durably bind it to the same tab and leaf. +4. Deliver the predecessor's physical daemon EXIT. The runtime becomes disconnected with an `exited` verdict. +5. Deliver the already-captured graph, without reordering renderer publications. +6. Resolve the pane, query the owning daemon's fresh inventory, and publish once more. A second variant unmounts the renderer terminal before that inventory and remounts it afterward through the real registration/publisher API. + +The replacement-binding commit and the daemon EXIT can run while a renderer invocation is queued. The production spawn commit persists the binding before returning its reply (`ipc/pty/ipc/spawn-commit-persist.ts`); the graph publisher reads the mounted pane's transport independently. The fixture controls that ordering; it does not prove its frequency on the reporting machine. + +Retirement correctly refuses to delete the successor's durable binding. Before the fix, the retained surface coordinates then allow the old graph leaf to set the predecessor connected again. A healthy inventory contains only the successor, but the runtime sweep skips records that still have a graph leaf. The list's presentation can label that leaf disconnected while the underlying pane resolver still sees it connected. + +## Results + +| Variant | First queued graph restores predecessor | After unmount, inventory, and remount | Pane conflict | +| --------------- | --------------------------------------- | ------------------------------------- | ----------------- | +| Before | yes | yes | yes | +| Exit check only | no | yes | no at first check | +| Complete fix | no | no | no | + +All three variants also run an ordinary-exit control without a replacement; it remains retired throughout. The middle variant isolates why a weak inventory absence during a renderer mount gap must preserve an already-earned exit certificate. Without that mount gap, the retained disconnected leaf keeps the inventory sweep from forgetting the verdict. + +The fix reuses the existing liveness verdict registry to keep an exited graph leaf disconnected and nonwritable, and skips recreating its PTY/URL-watcher ownership. It preserves surface membership: a separate actual `stopExactTerminalsForWorktree({ keepHistory: true })` control passes a changed renderer-built mobile snapshot while physical exit has completed but the stop reply is pending. The history surface remains present before and after the renderer clears its PTY binding. All phases check this control; the fixed phase also checks that mobile projection never combines one PTY's ID with another PTY's handle. + +Fresh spawn/registration clears the prior verdict; an owning inventory can establish `live`. A physical exit still records its certificate if the bounded PTY archive was already pruned. Host-only tests cover same-ID replacements, stale predecessor EXIT, fresh renderer-only panes, physical negative exit codes, local unverified stops, SSH disconnects, and retained history. The portable proof runs 15 actual-runtime cases across its three source variants. + +## Limits and version evidence + +The relevant graph admission, unconditional graph-connected write, durable retirement refusal, and inventory leaf exception are present in the reported **v1.4.197**. That tag already passes `providerExitObserved` from local and daemon physical exit callbacks and computes `processDeathCertified`; this proof's natural-exit path does not depend on #21000's synthetic-notification correction. Executable before/after runs use the current checkout with narrowly asserted source transforms, not the complete historical binary. + +This proves stale runtime/pane ownership, not a measured native-process or heap leak. The graph alone does not recreate a headless terminal model. It does not establish that every missing diagnostics row denotes an exited process, nor explain all handle-count growth in the issue. + +The existing register retains at most 256 unowned verdicts; PTY/handle/leaf owners keep their verdicts until their own lifecycle ends. The disconnected PTY archive is capped at 128. After both a record and its bounded verdict are evicted, the graph has no remaining per-ID certificate; this change does not add permanent tombstones. Later loss-of-contact writes can still replace an exit verdict with `unverifiable`; a stale positive inventory can separately write a connected record. Those paths are not exercised or fixed by this local queued-publication proof. + +A separate audit found that an unreachable unrelated legacy daemon can make aggregate exact-stop verification fail despite absence on the target's own daemon. That is excluded from this fix and from the proof's healthy target-inventory assertion. + +## Recorded run provenance + +`results.json` records the graph fix before the separate provider-inventory lifecycle fence in #21014. Its source hashes identify that earlier run; they are not a claim that every later audit commit has the same bytes. The reproduction can be rerun against the combined worktree. + +## Pull request dependency + +The graph PR is stacked on #21000, reusing its daemon socket fixture and physical-exit delivery contract. The graph mechanism is separate; the stack makes the executable proof dependencies explicit. diff --git a/docs/audits/queued-terminal-graph-exit/fixture.ts b/docs/audits/queued-terminal-graph-exit/fixture.ts new file mode 100644 index 00000000000..e145040e98a --- /dev/null +++ b/docs/audits/queued-terminal-graph-exit/fixture.ts @@ -0,0 +1,231 @@ +import { vi } from 'vitest' +import assert from 'node:assert/strict' +import { mkdtempSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { startLateExitHarness } from '../../../src/main/ipc/pty/daemon-late-exit-test-fixture' +import { OrcaRuntimeService } from '../../../src/main/runtime/orca-runtime' +import { Store } from '../../../src/main/persistence/loading-store/store' +import { resolveStablePaneOwner } from '../../../src/main/ipc/pty/pane/stable-owner' +import { + registerRuntimeTerminalTab, + setRuntimeGraphStoreStateGetter, + setRuntimeGraphSyncEnabled +} from '../../../src/renderer/src/runtime/sync-runtime-graph' +import { graphState } from '../../../src/renderer/src/runtime/sync-runtime-graph/graph-state' +import { syncRuntimeGraph } from '../../../src/renderer/src/runtime/sync-runtime-graph/graph-publication' +import { makeState } from '../../../src/renderer/src/runtime/sync-runtime-graph-test-harness' +import { advertisedUrlWatcher } from '../../../src/main/ports/advertised-url-watcher' + +class QueuedGraphExitRuntime extends OrcaRuntimeService { + capture(id: string) { + const pty = this.ptysById.get(id) + return { + connected: pty?.connected, + exitCause: pty?.lastExitCause, + incarnationId: pty?.incarnationId, + liveness: this.getPtyLivenessVerdict(id), + model: this.headlessTerminals.has(id), + urlBound: advertisedUrlWatcher['ptyToWorktree'].has(id), + leaves: this.getLeavesForPty(id).map((leaf) => ({ + connected: leaf.connected, + writable: leaf.writable + })) + } + } + + mobile(worktreeId: string) { + return this.getMobileSessionTabsForWorktree(worktreeId).tabs.flatMap((tab) => + tab.type === 'terminal' + ? [ + { + ptyId: tab.ptyId, + handlePtyId: tab.terminal ? this.handles.get(tab.terminal)?.ptyId : null + } + ] + : [] + ) + } +} +export async function runQueuedGraphExitScenario( + replacement: boolean, + graphGapBeforeInventory = false +) { + const h = await startLateExitHarness() + const predecessor = h.subprocess + const dir = mkdtempSync(join(tmpdir(), 'orca-queued-owner-')) + const store = new Store({ dataFile: join(dir, 'orca-data.json') }) + const runtime = new QueuedGraphExitRuntime(store) + h.session.runtime = runtime + const WT = 'repo::/tmp/late-exit-audit' + const TAB = '00000000-0000-4000-8000-000000000001' + const LEAF = '00000000-0000-4000-8000-000000000002' + const successorId = `${WT}@@successor` + let unregister: (() => void) | undefined + try { + store.persistPtyBinding({ + worktreeId: WT, + tabId: TAB, + leafId: LEAF, + ptyId: h.id, + incarnationId: h.result.incarnationId + }) + runtime.registerPty(h.id, WT, null, { + tabId: TAB, + leafId: LEAF, + incarnationId: h.result.incarnationId + }) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + listProcesses: (_connection, opts) => h.adapter.listProcesses(opts), + hasPty: (id) => h.adapter.hasPty(id) + }) + const state = makeState({ + tabsByWorktree: { + [WT]: [ + { + id: TAB, + worktreeId: WT, + title: 'Terminal', + ptyId: h.id, + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + terminalLayoutsByTabId: { + [TAB]: { + root: { type: 'leaf', leafId: LEAF }, + activeLeafId: LEAF, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF]: h.id } + } + } + }) + const manager = { + getPanes: () => [{ id: 1, leafId: LEAF }], + getActivePane: () => ({ id: 1, leafId: LEAF }), + getLeafId: () => LEAF, + getNumericIdForLeaf: () => 1 + } + setRuntimeGraphSyncEnabled(false) + setRuntimeGraphStoreStateGetter(() => state) + vi.stubGlobal('HTMLElement', class HTMLElement {}) + let deliver: (() => void) | undefined + let capture: unknown + let queue = false + vi.stubGlobal('window', { + api: { + runtime: { + syncWindowGraph: (graph: never) => { + if (!queue) { + return Promise.resolve(runtime.syncWindowGraph(1, graph)) + } + capture = structuredClone(graph) + return new Promise((resolve) => { + deliver = () => resolve(runtime.syncWindowGraph(1, graph)) + }) + } + } + } + }) + const mount = () => + registerRuntimeTerminalTab({ + tabId: TAB, + worktreeId: WT, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the publisher reads only the four pane lookup methods supplied by this headless manager. + getManager: () => manager as never, + getContainer: () => null, + getPtyIdForPane: () => h.id, + getTabWideAgentHintLeafId: () => null + }) + unregister = mount() + const publish = (): Promise => { + graphState.syncEnabled = true + const pending = syncRuntimeGraph() + graphState.syncEnabled = false + return pending + } + await publish() + queue = true + const inFlight = publish() + assert(deliver, 'Publisher did not dispatch its graph') + if (replacement) { + const next = await h.adapter.spawn({ cols: 80, rows: 24, sessionId: successorId }) + assert( + store.persistPtyBinding({ + worktreeId: WT, + tabId: TAB, + leafId: LEAF, + ptyId: next.id, + incarnationId: next.incarnationId + }) + ) + runtime.registerPty(next.id, WT, null, { + tabId: TAB, + leafId: LEAF, + incarnationId: next.incarnationId + }) + } + predecessor._simulateExit(0) + await h.waitForExit() + const afterExit = runtime.capture(h.id) + assert.equal(afterExit.connected, false) + deliver() + await inFlight + const afterQueuedGraph = runtime.capture(h.id) + let resolution: unknown + try { + resolution = resolveStablePaneOwner(runtime, store, `${TAB}:${LEAF}`, WT, null) + } catch (e) { + resolution = e instanceof Error ? e.message : String(e) + } + queue = false + if (graphGapBeforeInventory) { + unregister() + unregister = undefined + await publish() + } + const list = await runtime.listTerminals() + const afterFreshList = runtime.capture(h.id) + if (graphGapBeforeInventory) { + unregister = mount() + } + await publish() + return { + scenario: replacement ? 'successor-binding-before-exit' : 'ordinary-exit', + graphGapBeforeInventory, + capture, + afterExit, + afterQueuedGraph, + afterFreshList, + afterRepeatedGraph: runtime.capture(h.id), + mobile: runtime.mobile(WT), + resolution, + inventory: await h.adapter.listProcesses(), + persistedPtyId: + store.getWorkspaceSession().terminalLayoutsByTabId[TAB]?.ptyIdsByLeafId?.[LEAF], + listed: list.terminals.map((t) => ({ + id: t.ptyId, + connected: t.connected, + tabId: t.tabId, + leafId: t.leafId + })) + } + } finally { + graphState.syncEnabled = false + unregister?.() + setRuntimeGraphSyncEnabled(false) + setRuntimeGraphStoreStateGetter(null) + vi.unstubAllGlobals() + runtime.onPtyExit(h.id, 0) + runtime.onPtyExit(successorId, 0) + await h.dispose() + store.flushOrThrow() + rmSync(dir, { recursive: true, force: true }) + } +} diff --git a/docs/audits/queued-terminal-graph-exit/preserved-history-fixture.ts b/docs/audits/queued-terminal-graph-exit/preserved-history-fixture.ts new file mode 100644 index 00000000000..fd904d79ca1 --- /dev/null +++ b/docs/audits/queued-terminal-graph-exit/preserved-history-fixture.ts @@ -0,0 +1,180 @@ +import { expect, vi } from 'vitest' +import { OrcaRuntimeService } from '../../../src/main/runtime/orca-runtime' +import { + buildMobileSessionTabSnapshots, + registerRuntimeTerminalTab, + setRuntimeGraphStoreStateGetter, + setRuntimeGraphSyncEnabled +} from '../../../src/renderer/src/runtime/sync-runtime-graph' +import { makeState } from '../../../src/renderer/src/runtime/sync-runtime-graph-test-harness' +import { graphState } from '../../../src/renderer/src/runtime/sync-runtime-graph/graph-state' + +if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { + throw new Error('background required') +} + +const TAB = '10000000-0000-4000-8000-000000000001' +const TEST_WORKTREE_PATH = '/tmp/worktree-a' +const TEST_WORKTREE_ID = `repo-1::${TEST_WORKTREE_PATH}` +const LEAF = '10000000-0000-4000-8000-000000000002' +const INC = '10000000-0000-4000-8000-000000000003' +const PTY = `${TEST_WORKTREE_ID}@@sleep-review` + +export async function runPreservedHistoryScenario() { + const runtime = new OrcaRuntimeService() + const worktree = { + id: TEST_WORKTREE_ID, + path: TEST_WORKTREE_PATH, + repoId: 'repo-1', + name: 'worktree-a', + branch: 'main', + isMain: false + } + vi.spyOn(runtime, 'resolveWorktreeSelector').mockResolvedValue(worktree) + vi.spyOn(runtime, 'getResolvedWorktreeMap').mockResolvedValue( + new Map([[TEST_WORKTREE_ID, worktree]]) + ) + let stopped = false + let finishStop!: () => void + const stopGate = new Promise((resolve) => { + finishStop = resolve + }) + const stop = vi.fn(async () => { + runtime.onPtyExit(PTY, 0, INC, { providerExitObserved: true }) + stopped = true + await stopGate + return true + }) + runtime.setPtyController({ + write: () => true, + kill: () => true, + stopAndWait: stop, + getForegroundProcess: async () => null, + hasPty: () => !stopped, + listProcesses: async () => + stopped ? [] : [{ id: PTY, cwd: TEST_WORKTREE_PATH, title: 'terminal', incarnationId: INC }] + }) + let currentPty: string | null = PTY + const state = makeState({ + tabsByWorktree: { + [TEST_WORKTREE_ID]: [ + { + id: TAB, + worktreeId: TEST_WORKTREE_ID, + title: 'Terminal', + ptyId: PTY, + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + terminalLayoutsByTabId: { + [TAB]: { + root: { type: 'leaf', leafId: LEAF }, + activeLeafId: LEAF, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF]: PTY } + } + } + }) + const manager = { + getPanes: () => [{ id: 1, leafId: LEAF }], + getActivePane: () => ({ id: 1, leafId: LEAF }), + getLeafId: () => LEAF, + getNumericIdForLeaf: () => 1 + } + setRuntimeGraphSyncEnabled(false) + setRuntimeGraphStoreStateGetter(() => state) + const unregister = registerRuntimeTerminalTab({ + tabId: TAB, + worktreeId: TEST_WORKTREE_ID, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the snapshot builder reads only these four supplied pane lookup methods. + getManager: () => manager as never, + getContainer: () => null, + getPtyIdForPane: () => currentPty, + getTabWideAgentHintLeafId: () => null + }) + const publish = () => { + const snapshots = buildMobileSessionTabSnapshots(state) + runtime.syncWindowGraph(1, { + tabs: [ + { + tabId: TAB, + worktreeId: TEST_WORKTREE_ID, + title: 'Terminal', + activeLeafId: LEAF, + layout: null + } + ], + leaves: [ + { + tabId: TAB, + worktreeId: TEST_WORKTREE_ID, + leafId: LEAF, + paneRuntimeId: 1, + ptyId: currentPty + } + ], + mobileSessionTabs: snapshots + }) + return snapshots[0] + } + const capture = () => + runtime['mobileSessionTabsByWorktree'].get(TEST_WORKTREE_ID)?.tabs.map((tab) => ({ + type: tab.type, + id: tab.id, + ptyId: tab.type === 'terminal' ? tab.ptyId : null + })) + try { + runtime.registerPty(PTY, TEST_WORKTREE_ID, null, { + tabId: TAB, + leafId: LEAF, + incarnationId: INC + }) + publish() + const initial = capture() + expect(initial).toHaveLength(1) + const pending = runtime.stopExactTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`, [PTY], { + keepHistory: true, + targetOnly: true + }) + await vi.waitFor(() => expect(stop).toHaveBeenCalledOnce()) + const afterExit = capture() + expect(afterExit).toHaveLength(1) + state.runtimePaneTitlesByTabId = { [TAB]: { 1: 'Sleeping terminal' } } + const incoming = publish() + const afterQueued = capture() + const queuedLeaf = runtime['leaves'].get(runtime['getLeafKey'](TAB, LEAF)) + const leafState = queuedLeaf + ? { connected: queuedLeaf.connected, writable: queuedLeaf.writable, ptyId: queuedLeaf.ptyId } + : null + const model = runtime['headlessTerminals'].has(PTY) + finishStop() + const result = await pending + currentPty = null + state.tabsByWorktree[TEST_WORKTREE_ID] = [ + { ...state.tabsByWorktree[TEST_WORKTREE_ID][0], ptyId: null } + ] + state.terminalLayoutsByTabId[TAB] = { ...state.terminalLayoutsByTabId[TAB], ptyIdsByLeafId: {} } + publish() + return { + initial, + afterExit, + incoming, + afterQueued, + leafState, + model, + afterBindingClear: capture(), + result + } + } finally { + finishStop() + graphState.syncEnabled = false + unregister() + runtime.onPtyExit(PTY, 0, INC) + setRuntimeGraphStoreStateGetter(null) + vi.restoreAllMocks() + } +} diff --git a/docs/audits/queued-terminal-graph-exit/reproduce.mjs b/docs/audits/queued-terminal-graph-exit/reproduce.mjs new file mode 100644 index 00000000000..71e3ffede5a --- /dev/null +++ b/docs/audits/queued-terminal-graph-exit/reproduce.mjs @@ -0,0 +1,174 @@ +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 paths = [ + 'src/main/runtime/orca-runtime-sync-window-graph.ts', + 'src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts', + 'src/main/runtime/orca-runtime-on-pty-exit.ts' +] +const sources = await Promise.all(paths.map((path) => readFile(join(root, path), 'utf8'))) +const gate = ` // Retained history stays addressable, but a renderer graph cannot revoke a host-certified exit. + const connected = ptyId !== null && this.getPtyLivenessVerdict(ptyId)?.status !== 'exited' +` +const preserve = ` // An inventory's weak absence cannot revoke an earlier host-certified exit. + if (tracked?.verdict.status === 'exited') { + return + } +` +const certificate = ` if (processDeathCertified) { + // The bounded verdict register also fences late graphs after the PTY record was pruned. + this.rememberPtyLivenessVerdict(ptyId, { status: 'exited' }) + } +` +for (const [index, text] of [gate, preserve, certificate].entries()) { + assert(sources[index].includes(text), 'Source changed: review the baseline transform.') +} +const baseline = [ + sources[0] + .replace(gate, '') + .replace( + " connected,\n writable: this.graphStatus === 'ready' && connected,", + " connected: ptyId !== null,\n writable: this.graphStatus === 'ready' && ptyId !== null," + ) + .replace(' if (leaf.ptyId && connected) {', ' if (leaf.ptyId) {'), + sources[1].replace(preserve, ''), + sources[2].replace(certificate, '').replace( + ' pty.lastExitCause = exitCause\n', + ` pty.lastExitCause = exitCause + if (exitCode >= 0 || options.hostExitConfirmed === true) { + this.rememberPtyLivenessVerdict(ptyId, { status: 'exited' }) + } +` + ) +] +const scratch = await mkdtemp(join(tmpdir(), 'orca-queued-graph-proof-')) +const phases = [] +try { + for (const phase of ['before', 'guard-only', 'after']) { + const outputPath = 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 { runQueuedGraphExitScenario } from ${JSON.stringify(join(root, 'docs/audits/queued-terminal-graph-exit/fixture.ts'))} +import { runPreservedHistoryScenario } from ${JSON.stringify(join(root, 'docs/audits/queued-terminal-graph-exit/preserved-history-fixture.ts'))} +const rows = [] +let history +it("preserved history", async () => { history = await runPreservedHistoryScenario() }) +for (const successor of [false, true]) { + for (const graphGapBeforeInventory of [false, true]) { + it(String(successor) + String(graphGapBeforeInventory), async () => rows.push(await runQueuedGraphExitScenario(successor, graphGapBeforeInventory))) + } +} +afterAll(() => writeFileSync(${JSON.stringify(outputPath)}, JSON.stringify({ rows, history }))) +` + ) + const replacements = Object.fromEntries( + paths.map((path, index) => [ + `/${path}`, + phase === 'after' || (phase === 'guard-only' && index === 0) + ? sources[index] + : baseline[index] + ]) + ) + await writeFile( + configPath, + ` +import base from ${JSON.stringify(pathToFileURL(join(root, 'config/vitest.config.ts')).href)} +const replacements = ${JSON.stringify(replacements)} +export default { + ...base, + plugins: [{ name: 'graph-exit-baseline', enforce: 'pre', transform(code, id) { + for (const [path, replacement] of Object.entries(replacements)) { + if (id.replaceAll('\\\\', '/').endsWith(path)) return replacement + } + } }], + 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 { rows, history } = JSON.parse(await readFile(outputPath, 'utf8')) + assert.equal(history.afterQueued.length, 1) + assert.equal(history.afterBindingClear.length, 1) + assert.equal(history.model, false) + if (phase !== 'before') { + assert.equal(history.leafState.connected, false) + assert.equal(history.leafState.writable, false) + } + delete history.incoming.publicationEpoch + assert.equal(rows.length, 4) + for (const row of rows) { + const successor = row.scenario === 'successor-binding-before-exit' + assert.equal(row.afterExit.connected, false) + assert.equal(row.afterQueuedGraph.connected, successor && phase === 'before') + assert.equal( + row.afterRepeatedGraph.connected, + successor && (phase === 'before' || (phase === 'guard-only' && row.graphGapBeforeInventory)) + ) + assert.equal( + row.resolution === 'terminal_pane_owner_conflict', + successor && phase === 'before' + ) + assert.equal(row.inventory.length, successor ? 1 : 0) + assert.equal(row.afterRepeatedGraph.model, false) + if (phase === 'after') { + assert.equal(row.afterRepeatedGraph.urlBound, false) + assert(row.afterRepeatedGraph.leaves.every((leaf) => !leaf.connected && !leaf.writable)) + assert(row.mobile.every((tab) => !tab.handlePtyId || tab.ptyId === tab.handlePtyId)) + } + for (const state of [ + row.afterExit, + row.afterQueuedGraph, + row.afterFreshList, + row.afterRepeatedGraph + ]) { + delete state.incarnationId + } + delete row.capture.rendererGeneration + if (row.resolution && typeof row.resolution === 'object') { + row.resolution = { ptyId: row.resolution.ptyId } + } + row.inventory = row.inventory.map(({ id }) => ({ id })) + } + phases.push({ phase, rows, history }) + } + const output = `${JSON.stringify( + { + sources: Object.fromEntries( + paths.map((path, index) => [ + path, + createHash('sha256').update(sources[index]).digest('hex') + ]) + ), + phases + }, + 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 }) +} diff --git a/docs/audits/queued-terminal-graph-exit/results.json b/docs/audits/queued-terminal-graph-exit/results.json new file mode 100644 index 00000000000..515d114eb1f --- /dev/null +++ b/docs/audits/queued-terminal-graph-exit/results.json @@ -0,0 +1,1763 @@ +{ + "sources": { + "src/main/runtime/orca-runtime-sync-window-graph.ts": "e9527f5964af3ed93ec4815096ebdcc181118f9edc76f34e1cc25d1fa67a5fb4", + "src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts": "ff5433a848e43f22560234c98e25122237f16bcda7668603dd2640eb86cec315", + "src/main/runtime/orca-runtime-on-pty-exit.ts": "def5d33f70a656b13c619db8b453ab2b72cade31c7078583c6f6f6ea8d3544e6" + }, + "phases": [ + { + "phase": "before", + "rows": [ + { + "scenario": "ordinary-exit", + "graphGapBeforeInventory": false, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [], + "unchangedMobileSessionWorktrees": ["repo::/tmp/late-exit-audit"] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "mobile": [], + "resolution": null, + "inventory": [], + "listed": [] + }, + { + "scenario": "ordinary-exit", + "graphGapBeforeInventory": true, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:852007b5-0f95-44b0-9182-1dbd97a4ce2b", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "mobile": [], + "resolution": null, + "inventory": [], + "listed": [] + }, + { + "scenario": "successor-binding-before-exit", + "graphGapBeforeInventory": false, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:852007b5-0f95-44b0-9182-1dbd97a4ce2b", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": true, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": true, + "leaves": [ + { + "connected": true, + "writable": true + } + ] + }, + "afterFreshList": { + "connected": true, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": true, + "leaves": [ + { + "connected": true, + "writable": true + } + ] + }, + "afterRepeatedGraph": { + "connected": true, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": true, + "leaves": [ + { + "connected": true, + "writable": true + } + ] + }, + "mobile": [ + { + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "handlePtyId": "repo::/tmp/late-exit-audit@@terminal" + } + ], + "resolution": "terminal_pane_owner_conflict", + "inventory": [ + { + "id": "repo::/tmp/late-exit-audit@@successor" + } + ], + "persistedPtyId": "repo::/tmp/late-exit-audit@@successor", + "listed": [ + { + "id": "repo::/tmp/late-exit-audit@@terminal", + "connected": false, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + { + "id": "repo::/tmp/late-exit-audit@@successor", + "connected": true, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + } + ] + }, + { + "scenario": "successor-binding-before-exit", + "graphGapBeforeInventory": true, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:852007b5-0f95-44b0-9182-1dbd97a4ce2b", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": true, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": true, + "leaves": [ + { + "connected": true, + "writable": true + } + ] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": true, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": true, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": true, + "leaves": [ + { + "connected": true, + "writable": true + } + ] + }, + "mobile": [ + { + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "handlePtyId": "repo::/tmp/late-exit-audit@@terminal" + } + ], + "resolution": "terminal_pane_owner_conflict", + "inventory": [ + { + "id": "repo::/tmp/late-exit-audit@@successor" + } + ], + "persistedPtyId": "repo::/tmp/late-exit-audit@@successor", + "listed": [ + { + "id": "repo::/tmp/late-exit-audit@@successor", + "connected": true, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + } + ] + } + ], + "history": { + "initial": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "afterExit": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "incoming": { + "worktree": "repo-1::/tmp/worktree-a", + "snapshotVersion": 2, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "title": "Sleeping terminal", + "parentTabId": "10000000-0000-4000-8000-000000000001", + "leafId": "10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "10000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "10000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "10000000-0000-4000-8000-000000000002": "repo-1::/tmp/worktree-a@@sleep-review" + } + }, + "isActive": false + } + ] + }, + "afterQueued": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "leafState": { + "connected": true, + "writable": true, + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + }, + "model": false, + "afterBindingClear": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": null + } + ], + "result": { + "stopped": 1, + "stoppedPtyIds": ["repo-1::/tmp/worktree-a@@sleep-review"], + "livePtyIds": ["repo-1::/tmp/worktree-a@@sleep-review"], + "postStopVerified": true + } + } + }, + { + "phase": "guard-only", + "rows": [ + { + "scenario": "ordinary-exit", + "graphGapBeforeInventory": false, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [], + "unchangedMobileSessionWorktrees": ["repo::/tmp/late-exit-audit"] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "mobile": [], + "resolution": null, + "inventory": [], + "listed": [] + }, + { + "scenario": "ordinary-exit", + "graphGapBeforeInventory": true, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:3f6ce3d1-c184-4637-a5c1-14b30cd0b1b4", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "mobile": [], + "resolution": null, + "inventory": [], + "listed": [] + }, + { + "scenario": "successor-binding-before-exit", + "graphGapBeforeInventory": false, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:3f6ce3d1-c184-4637-a5c1-14b30cd0b1b4", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "mobile": [ + { + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "handlePtyId": null + } + ], + "resolution": { + "ptyId": "repo::/tmp/late-exit-audit@@successor" + }, + "inventory": [ + { + "id": "repo::/tmp/late-exit-audit@@successor" + } + ], + "persistedPtyId": "repo::/tmp/late-exit-audit@@successor", + "listed": [ + { + "id": "repo::/tmp/late-exit-audit@@terminal", + "connected": false, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + { + "id": "repo::/tmp/late-exit-audit@@successor", + "connected": true, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + } + ] + }, + { + "scenario": "successor-binding-before-exit", + "graphGapBeforeInventory": true, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:3f6ce3d1-c184-4637-a5c1-14b30cd0b1b4", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": true, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": null, + "model": false, + "urlBound": true, + "leaves": [ + { + "connected": true, + "writable": true + } + ] + }, + "mobile": [ + { + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "handlePtyId": "repo::/tmp/late-exit-audit@@terminal" + } + ], + "resolution": { + "ptyId": "repo::/tmp/late-exit-audit@@successor" + }, + "inventory": [ + { + "id": "repo::/tmp/late-exit-audit@@successor" + } + ], + "persistedPtyId": "repo::/tmp/late-exit-audit@@successor", + "listed": [ + { + "id": "repo::/tmp/late-exit-audit@@successor", + "connected": true, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + } + ] + } + ], + "history": { + "initial": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "afterExit": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "incoming": { + "worktree": "repo-1::/tmp/worktree-a", + "snapshotVersion": 2, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "title": "Sleeping terminal", + "parentTabId": "10000000-0000-4000-8000-000000000001", + "leafId": "10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "10000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "10000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "10000000-0000-4000-8000-000000000002": "repo-1::/tmp/worktree-a@@sleep-review" + } + }, + "isActive": false + } + ] + }, + "afterQueued": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "leafState": { + "connected": false, + "writable": false, + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + }, + "model": false, + "afterBindingClear": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": null + } + ], + "result": { + "stopped": 1, + "stoppedPtyIds": ["repo-1::/tmp/worktree-a@@sleep-review"], + "livePtyIds": ["repo-1::/tmp/worktree-a@@sleep-review"], + "postStopVerified": true + } + } + }, + { + "phase": "after", + "rows": [ + { + "scenario": "ordinary-exit", + "graphGapBeforeInventory": false, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [], + "unchangedMobileSessionWorktrees": ["repo::/tmp/late-exit-audit"] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "mobile": [], + "resolution": null, + "inventory": [], + "listed": [] + }, + { + "scenario": "ordinary-exit", + "graphGapBeforeInventory": true, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:34668074-f3d3-4e0f-bbd5-e31900482d81", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "mobile": [], + "resolution": null, + "inventory": [], + "listed": [] + }, + { + "scenario": "successor-binding-before-exit", + "graphGapBeforeInventory": false, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:34668074-f3d3-4e0f-bbd5-e31900482d81", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "mobile": [ + { + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "handlePtyId": null + } + ], + "resolution": { + "ptyId": "repo::/tmp/late-exit-audit@@successor" + }, + "inventory": [ + { + "id": "repo::/tmp/late-exit-audit@@successor" + } + ], + "persistedPtyId": "repo::/tmp/late-exit-audit@@successor", + "listed": [ + { + "id": "repo::/tmp/late-exit-audit@@terminal", + "connected": false, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + { + "id": "repo::/tmp/late-exit-audit@@successor", + "connected": true, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + } + ] + }, + { + "scenario": "successor-binding-before-exit", + "graphGapBeforeInventory": true, + "capture": { + "tabs": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "title": "Terminal", + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "layout": null + } + ], + "leaves": [ + { + "tabId": "00000000-0000-4000-8000-000000000001", + "worktreeId": "repo::/tmp/late-exit-audit", + "leafId": "00000000-0000-4000-8000-000000000002", + "paneRuntimeId": 1, + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "paneTitle": null, + "title": "Terminal" + } + ], + "mobileSessionTabs": [ + { + "worktree": "repo::/tmp/late-exit-audit", + "publicationEpoch": "renderer:34668074-f3d3-4e0f-bbd5-e31900482d81", + "snapshotVersion": 4, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "00000000-0000-4000-8000-000000000001::00000000-0000-4000-8000-000000000002", + "title": "Terminal", + "parentTabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002", + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "00000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "00000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "00000000-0000-4000-8000-000000000002": "repo::/tmp/late-exit-audit@@terminal" + } + }, + "isActive": false + } + ] + } + ], + "unchangedMobileSessionWorktrees": [] + }, + "afterExit": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterQueuedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "afterFreshList": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [] + }, + "afterRepeatedGraph": { + "connected": false, + "exitCause": { + "kind": "exited", + "exitCode": 0 + }, + "liveness": { + "status": "exited" + }, + "model": false, + "urlBound": false, + "leaves": [ + { + "connected": false, + "writable": false + } + ] + }, + "mobile": [ + { + "ptyId": "repo::/tmp/late-exit-audit@@terminal", + "handlePtyId": null + } + ], + "resolution": { + "ptyId": "repo::/tmp/late-exit-audit@@successor" + }, + "inventory": [ + { + "id": "repo::/tmp/late-exit-audit@@successor" + } + ], + "persistedPtyId": "repo::/tmp/late-exit-audit@@successor", + "listed": [ + { + "id": "repo::/tmp/late-exit-audit@@successor", + "connected": true, + "tabId": "00000000-0000-4000-8000-000000000001", + "leafId": "00000000-0000-4000-8000-000000000002" + } + ] + } + ], + "history": { + "initial": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "afterExit": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "incoming": { + "worktree": "repo-1::/tmp/worktree-a", + "snapshotVersion": 2, + "activeGroupId": null, + "activeTabId": null, + "activeTabType": null, + "tabs": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "title": "Sleeping terminal", + "parentTabId": "10000000-0000-4000-8000-000000000001", + "leafId": "10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review", + "parentLayout": { + "root": { + "type": "leaf", + "leafId": "10000000-0000-4000-8000-000000000002" + }, + "activeLeafId": "10000000-0000-4000-8000-000000000002", + "expandedLeafId": null, + "ptyIdsByLeafId": { + "10000000-0000-4000-8000-000000000002": "repo-1::/tmp/worktree-a@@sleep-review" + } + }, + "isActive": false + } + ] + }, + "afterQueued": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + } + ], + "leafState": { + "connected": false, + "writable": false, + "ptyId": "repo-1::/tmp/worktree-a@@sleep-review" + }, + "model": false, + "afterBindingClear": [ + { + "type": "terminal", + "id": "10000000-0000-4000-8000-000000000001::10000000-0000-4000-8000-000000000002", + "ptyId": null + } + ], + "result": { + "stopped": 1, + "stoppedPtyIds": ["repo-1::/tmp/worktree-a@@sleep-review"], + "livePtyIds": ["repo-1::/tmp/worktree-a@@sleep-review"], + "postStopVerified": true + } + } + } + ] +} diff --git a/src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts b/src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts index e86e10e41c3..c4e4324e47d 100644 --- a/src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts +++ b/src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts @@ -137,6 +137,10 @@ export class OrcaRuntimeWithMarkPtyLivenessUnverifiable extends OrcaRuntimeWithO protected forgetPtyLivenessVerdict(ptyId: string, observedNoLaterThan?: number): void { const tracked = this.ptyLivenessVerdictByPtyId.get(ptyId) + // An inventory's weak absence cannot revoke an earlier host-certified exit. + if (tracked?.verdict.status === 'exited') { + return + } if (observedNoLaterThan !== undefined && tracked && tracked.observedAt > observedNoLaterThan) { return } diff --git a/src/main/runtime/orca-runtime-on-pty-exit.ts b/src/main/runtime/orca-runtime-on-pty-exit.ts index 6ddf877f87c..33e1dfc5078 100644 --- a/src/main/runtime/orca-runtime-on-pty-exit.ts +++ b/src/main/runtime/orca-runtime-on-pty-exit.ts @@ -198,6 +198,10 @@ export class OrcaRuntimeWithOnPtyExit extends OrcaRuntimeWithOnClientDisconnecte this.terminalDrivers.clear(ptyId) this.remoteDesktopFloor.clearPty(ptyId) this.disposeHeadlessTerminal(ptyId) + if (processDeathCertified) { + // The bounded verdict register also fences late graphs after the PTY record was pruned. + this.rememberPtyLivenessVerdict(ptyId, { status: 'exited' }) + } if (pty) { pty.connected = false pty.runtimeSessionOwned = false @@ -205,12 +209,6 @@ export class OrcaRuntimeWithOnPtyExit extends OrcaRuntimeWithOnClientDisconnecte pty.disconnectedAt = Date.now() pty.lastExitCode = exitCode pty.lastExitCause = exitCause - if (exitCode >= 0 || options.hostExitConfirmed === true) { - // Record the certificate rather than merely dropping the doubt: a reader that has to - // authorize a respawn cannot distinguish "the host reported this process gone" from "this - // runtime has never asked" if both are absence. - this.rememberPtyLivenessVerdict(ptyId, { status: 'exited' }) - } // Why: the exited process's live frames say nothing about a replacement. // A same-id respawn makes the leaf writable again before any new title, // so leaving this true would let push delivery type into the new process diff --git a/src/main/runtime/orca-runtime-sync-window-graph.ts b/src/main/runtime/orca-runtime-sync-window-graph.ts index e820d15130b..c7f261f88be 100644 --- a/src/main/runtime/orca-runtime-sync-window-graph.ts +++ b/src/main/runtime/orca-runtime-sync-window-graph.ts @@ -107,14 +107,16 @@ export class OrcaRuntimeWithSyncWindowGraph extends OrcaRuntimeWithAttachWindow ? existing.ptyGeneration + 1 : (existing?.ptyGeneration ?? 0) const existingPty = ptyId ? this.ptysById.get(ptyId) : undefined + // Retained history stays addressable, but a renderer graph cannot revoke a host-certified exit. + const connected = ptyId !== null && this.getPtyLivenessVerdict(ptyId)?.status !== 'exited' const tailSource = existing?.ptyId === ptyId ? existing : existingPty nextLeaves.set(leafKey, { ...leaf, ptyId, ptyGeneration, - connected: ptyId !== null, - writable: this.graphStatus === 'ready' && ptyId !== null, + connected, + writable: this.graphStatus === 'ready' && connected, lastOutputAt: tailSource?.lastOutputAt ?? null, lastExitCode: tailSource?.lastExitCode ?? null, lastExitCause: tailSource?.lastExitCause ?? null, @@ -138,7 +140,7 @@ export class OrcaRuntimeWithSyncWindowGraph extends OrcaRuntimeWithAttachWindow : graphSyncedAt }) - if (leaf.ptyId) { + if (leaf.ptyId && connected) { this.recordPtyWorktree(leaf.ptyId, leaf.worktreeId, { connected: true, lastOutputAt: existing?.ptyId === leaf.ptyId ? existing.lastOutputAt : null, diff --git a/src/main/runtime/queued-terminal-graph-exit.test.ts b/src/main/runtime/queued-terminal-graph-exit.test.ts new file mode 100644 index 00000000000..a8a7be5d92e --- /dev/null +++ b/src/main/runtime/queued-terminal-graph-exit.test.ts @@ -0,0 +1,251 @@ +import { describe, expect, it, vi } from 'vitest' +import { OrcaRuntimeService } from './orca-runtime' + +const WORKTREE = 'repo::/tmp/graph-exit' +const TAB = '10000000-0000-4000-8000-000000000001' +const LEAF = '10000000-0000-4000-8000-000000000002' +const PTY = `${WORKTREE}@@terminal` +const FIRST = '10000000-0000-4000-8000-000000000003' +const NEXT = '10000000-0000-4000-8000-000000000004' + +class ExitAuthorityRuntime extends OrcaRuntimeService { + override resolveWorktreeSelector(selector: string) { + return super.resolveWorktreeSelector(selector) + } + + override getResolvedWorktreeMap() { + return super.getResolvedWorktreeMap() + } + + capture(id = PTY) { + const pty = this.ptysById.get(id) + return { connected: pty?.connected, incarnationId: pty?.incarnationId } + } + + get verdictCount(): number { + return this.ptyLivenessVerdictByPtyId.size + } + + dropRecord(id = PTY): void { + this.dropDisconnectedPtyRecord(id) + } + + history() { + return { + surfaces: this.mobileSessionTabsByWorktree.get(WORKTREE)?.tabs.length, + leaves: this.getLeavesForPty(PTY).map((leaf) => ({ + connected: leaf.connected, + writable: leaf.writable + })), + model: this.headlessTerminals.has(PTY) + } + } +} + +function graph( + runtime: OrcaRuntimeService, + ptyId: string | null = PTY, + snapshotVersion?: number +): void { + runtime.syncWindowGraph(1, { + tabs: [ + { tabId: TAB, worktreeId: WORKTREE, title: 'terminal', activeLeafId: LEAF, layout: null } + ], + leaves: [{ tabId: TAB, worktreeId: WORKTREE, leafId: LEAF, paneRuntimeId: 1, ptyId }], + ...(snapshotVersion === undefined + ? {} + : { + mobileSessionTabs: [ + { + worktree: WORKTREE, + publicationEpoch: 'renderer:retained-history', + snapshotVersion, + activeGroupId: null, + activeTabId: `${TAB}::${LEAF}`, + activeTabType: 'terminal' as const, + tabs: [ + { + type: 'terminal' as const, + id: `${TAB}::${LEAF}`, + parentTabId: TAB, + leafId: LEAF, + ...(ptyId ? { ptyId } : {}), + title: 'Terminal', + isActive: true + } + ] + } + ] + }) + }) +} + +function register(runtime: OrcaRuntimeService, incarnationId = FIRST): void { + runtime.registerPty(PTY, WORKTREE, null, { tabId: TAB, leafId: LEAF, incarnationId }) +} + +describe('host exit authority over queued renderer graphs', () => { + it('keeps exact-stop history addressable through a changed snapshot before binding clears', async () => { + const runtime = new ExitAuthorityRuntime() + const git = { + path: '/tmp/graph-exit', + head: 'abc', + branch: 'main', + isBare: false, + isMainWorktree: false + } + const worktree = { + ...git, + git, + id: WORKTREE, + repoId: 'repo', + displayName: 'graph-exit', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + parentWorktreeId: null, + childWorktreeIds: [], + lineage: null + } + const resolve = vi.spyOn(runtime, 'resolveWorktreeSelector').mockResolvedValue(worktree) + const map = vi + .spyOn(runtime, 'getResolvedWorktreeMap') + .mockResolvedValue(new Map([[WORKTREE, worktree]])) + let stopped = false + let finishStop!: () => void + const gate = new Promise((done) => { + finishStop = done + }) + const stop = vi.fn(async () => { + runtime.onPtyExit(PTY, 0, FIRST, { providerExitObserved: true }) + stopped = true + await gate + return true + }) + runtime.setPtyController({ + write: () => true, + kill: () => true, + stopAndWait: stop, + getForegroundProcess: async () => null, + hasPty: () => !stopped, + listProcesses: async () => + stopped ? [] : [{ id: PTY, incarnationId: FIRST, cwd: worktree.path, title: 'terminal' }] + }) + let pending: Promise | undefined + try { + register(runtime) + graph(runtime, PTY, 1) + pending = runtime.stopExactTerminalsForWorktree(`id:${WORKTREE}`, [PTY], { + keepHistory: true, + targetOnly: true + }) + await vi.waitFor(() => expect(stop).toHaveBeenCalledOnce()) + graph(runtime, PTY, 2) + expect(runtime.history()).toEqual({ + surfaces: 1, + leaves: [{ connected: false, writable: false }], + model: false + }) + finishStop() + await expect(pending).resolves.toMatchObject({ postStopVerified: true }) + graph(runtime, null, 3) + expect(runtime.history().surfaces).toBe(1) + } finally { + finishStop() + await pending + resolve.mockRestore() + map.mockRestore() + runtime.onPtyExit(PTY, 0, FIRST) + } + }) + + it.each([0, -1])('retains a physical exit certificate after record pruning, code=%s', (code) => { + const runtime = new ExitAuthorityRuntime() + register(runtime) + runtime.dropRecord() + runtime.onPtyExit(PTY, code, FIRST, { providerExitObserved: true }) + graph(runtime) + expect(runtime.capture().connected).toBeUndefined() + expect(runtime.getPtyLivenessVerdict(PTY)).toEqual({ status: 'exited' }) + }) + + it('admits a new renderer pane without inventing a host verdict', () => { + const runtime = new ExitAuthorityRuntime() + graph(runtime) + expect(runtime.capture().connected).toBe(true) + expect(runtime.getPtyLivenessVerdict(PTY)).toBeNull() + }) + + it('admits a registered successor and ignores the predecessor exit', () => { + const runtime = new ExitAuthorityRuntime() + register(runtime) + runtime.onPtyExit(PTY, 0, FIRST) + register(runtime, NEXT) + runtime.onPtyExit(PTY, 0, FIRST) + graph(runtime) + expect(runtime.capture()).toEqual({ connected: true, incarnationId: NEXT }) + expect(runtime.getPtyLivenessVerdict(PTY)).toBeNull() + }) + + it('admits a same-ID spawn before its registration commits', () => { + const runtime = new ExitAuthorityRuntime() + register(runtime) + runtime.onPtyExit(PTY, 0, FIRST) + runtime.onPtySpawned(PTY, NEXT) + graph(runtime) + expect(runtime.capture()).toEqual({ connected: true, incarnationId: NEXT }) + expect(runtime.getPtyLivenessVerdict(PTY)).toBeNull() + }) + + it('allows owning inventory to prove the same ID live again', async () => { + const runtime = new ExitAuthorityRuntime() + register(runtime) + runtime.onPtyExit(PTY, 0, FIRST) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + listProcesses: async () => [{ id: PTY, incarnationId: NEXT, cwd: '', title: 'terminal' }] + }) + await runtime.listTerminals() + graph(runtime) + expect(runtime.capture()).toEqual({ connected: true, incarnationId: NEXT }) + expect(runtime.getPtyLivenessVerdict(PTY)?.status).toBe('live') + }) + + it('keeps an SSH disconnect unverifiable and admissible', () => { + const runtime = new ExitAuthorityRuntime() + const id = 'ssh:target@@terminal' + runtime.registerPty(id, WORKTREE, 'target', { tabId: TAB, leafId: LEAF, incarnationId: FIRST }) + runtime.onPtyExit(id, -1, FIRST) + graph(runtime, id) + expect(runtime.getPtyLivenessVerdict(id)?.status).toBe('unverifiable') + expect(runtime.capture(id).connected).toBe(true) + }) + + it('does not promote an unverified local stop to an exit certificate', () => { + const runtime = new ExitAuthorityRuntime() + runtime.registerPty(PTY, WORKTREE) + runtime.onPtyExit(PTY, -1, FIRST) + runtime.markPtyLivenessUnverifiable(PTY, 'stop unverified') + graph(runtime) + expect(runtime.getPtyLivenessVerdict(PTY)?.status).toBe('unverifiable') + expect(runtime.capture().connected).toBe(true) + }) + + it('bounds certificates for exits whose PTY records are already gone', () => { + const runtime = new ExitAuthorityRuntime() + for (let index = 0; index < 1_000; index++) { + runtime.onPtyExit(`${PTY}-${index}`, 0) + } + expect(runtime.verdictCount).toBe(256) + expect(runtime.getPtyLivenessVerdict(`${PTY}-0`)).toBeNull() + expect(runtime.getPtyLivenessVerdict(`${PTY}-999`)).toEqual({ status: 'exited' }) + }) +}) From f0dfc5de7b8c00c833c36eb83edfedfe95dbaeea Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:31:37 -0700 Subject: [PATCH 31/59] fix(projects): release processed repository scan records (#21022) Co-authored-by: m4air --- .../nested-repo-processed-queue/README.md | 34 +++ .../nested-repo-processed-queue/fix.patch | 28 ++ .../nested-repo-processed-queue/reproduce.cjs | 286 ++++++++++++++++++ .../nested-repo-processed-queue/results.json | 74 +++++ .../nested-repo-discovery-queue.test.ts | 122 ++++++++ .../project-groups/nested-repo-discovery.ts | 10 +- 6 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 docs/audits/nested-repo-processed-queue/README.md create mode 100644 docs/audits/nested-repo-processed-queue/fix.patch create mode 100644 docs/audits/nested-repo-processed-queue/reproduce.cjs create mode 100644 docs/audits/nested-repo-processed-queue/results.json create mode 100644 src/main/project-groups/nested-repo-discovery-queue.test.ts diff --git a/docs/audits/nested-repo-processed-queue/README.md b/docs/audits/nested-repo-processed-queue/README.md new file mode 100644 index 00000000000..c5ef32e0a2b --- /dev/null +++ b/docs/audits/nested-repo-processed-queue/README.md @@ -0,0 +1,34 @@ +# Release completed nested-repository scan records + +`scanNestedRepos` kept every consumed `TraversalFolder` in its breadth-first queue until the scan finished. Those records retained path segments and inherited parsed ignore rules after their directories had been processed. Releasing each consumed slot and occasionally compacting the empty prefix removes that temporary retention while preserving traversal order. + +## Run + +```sh +ORCA_BACKGROUND_LAUNCH=1 node docs/audits/nested-repo-processed-queue/reproduce.cjs +``` + +The runner uses the repository's process launcher to start a Node child with forced GC, a 256 MiB old-space limit and a 15-second timeout. It bundles the actual scan and ignore-rule parser. An observational hook records weak references and scalar queue counts. A finite injected filesystem pauses one directory read; no app window, PTY, SSH connection or native watcher starts. The unused local Git detector is a throwing stub, ensuring the injected filesystem owns every probe. + +The fixture has 96 branches, each with 64 distinct ignore rules and one child directory. It pauses the penultimate child read, leaving one pending directory. Four event-loop-separated GC rounds precede each observation. + +| Observation | Before | Slot-release control | Fixed | +| ------------------------------------------ | ------: | -------------------: | -----: | +| Completed child records surviving GC | 94 / 94 | 0 / 94 | 0 / 94 | +| Their inherited rule arrays surviving GC | 94 / 94 | 0 / 94 | 0 / 94 | +| Observed records surviving scan completion | 0 | 0 | 0 | +| Total directories visited | 193 | 193 | 193 | + +All variants visit the same directories in exactly the same order and return the same empty result. The baseline reverses only `fix.patch` in memory. The diagnostic control adds only consumed-slot clearing to that baseline, without compaction; it isolates the retaining path. The fixed variant executes the current queue implementation. `results.json` includes source hashes, exact queue counts, runtime provenance, process exit and timeout status. + +The narrow source regression suite exercises a wider traversal across repeated compaction, including Windows and SSH POSIX path forms, inherited ignore rules, breadth-first result order, maximum depth, repository caps, cancellation and optional timeout behavior. All 37 discovery, queue and scan-rule tests passed, along with Node typechecking and focused lint checks. Existing discovery tests cover local filesystem and symlink behavior. + +## Reuse and scope + +The change follows the consumed-slot release pattern in `ws-outbound-backpressure-queue.ts` and the amortized prefix compaction pattern in `runtime-rpc-call-queue.ts`. It introduces no new queue abstraction, traversal policy, RPC field or host boundary. + +The baseline source matches `v1.4.198`; the runner verifies this named-tag comparison after normalizing CRLF line endings to LF for Windows checkout portability. This is not an execution of the historical packaged application. + +The IPC route uses this scanner for local and SSH-backed folder selection, with filesystem operations delegated to the selected host. Runtime scan/import routes request a 15-second timeout; IPC forwards options, whose timeout defaults to null. Existing time checks happen between awaited operations and do not cancel a pending read. + +This fix releases completed work. It does not cap the active frontier, directory entry arrays, `.gitignore` size or directory breadth. The original implementation releases its records on scan completion. No heap-byte savings or field-incident attribution is claimed; no affected-host data was used. diff --git a/docs/audits/nested-repo-processed-queue/fix.patch b/docs/audits/nested-repo-processed-queue/fix.patch new file mode 100644 index 00000000000..b79d294abb4 --- /dev/null +++ b/docs/audits/nested-repo-processed-queue/fix.patch @@ -0,0 +1,28 @@ +diff --git a/src/main/project-groups/nested-repo-discovery.ts b/src/main/project-groups/nested-repo-discovery.ts +index 84fddfd116..1f25a85a26 100644 +--- a/src/main/project-groups/nested-repo-discovery.ts ++++ b/src/main/project-groups/nested-repo-discovery.ts +@@ -90,7 +90,7 @@ export async function scanNestedRepos(args: { + return buildResult('non_git_folder') + } + +- const foldersToTraverse: TraversalFolder[] = [ ++ const foldersToTraverse: (TraversalFolder | undefined)[] = [ + { path: args.path, depth: 0, segments: [], ignoreRules: [] } + ] + let nextFolderIndex = 0 +@@ -107,7 +107,13 @@ export async function scanNestedRepos(args: { + if (noteAbort()) { + break + } +- const currentFolder = foldersToTraverse[nextFolderIndex++] ++ const currentFolder = foldersToTraverse[nextFolderIndex++]! ++ // Release processed paths and inherited ignore rules before the next filesystem await. ++ foldersToTraverse[nextFolderIndex - 1] = undefined ++ if (nextFolderIndex >= 64 && nextFolderIndex * 2 >= foldersToTraverse.length) { ++ foldersToTraverse.splice(0, nextFolderIndex) ++ nextFolderIndex = 0 ++ } + if (currentFolder.depth > options.maxDepth) { + continue + } diff --git a/docs/audits/nested-repo-processed-queue/reproduce.cjs b/docs/audits/nested-repo-processed-queue/reproduce.cjs new file mode 100644 index 00000000000..17fd5874c35 --- /dev/null +++ b/docs/audits/nested-repo-processed-queue/reproduce.cjs @@ -0,0 +1,286 @@ +const assert = require('node:assert/strict') +const { createHash } = require('node:crypto') +const { readFileSync, mkdtempSync, rmSync } = require('node:fs') +const { tmpdir } = require('node:os') +const { join, resolve } = require('node:path') +const { build } = require('esbuild') +const { applyPatch, parsePatch, reversePatch } = require('diff') + +if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { + throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1') +} +if (process.argv[2] === '--proof-child' && typeof global.gc !== 'function') { + throw new Error('Child proof requires --expose-gc') +} +const root = resolve(__dirname, '../../..') +const readSource = (path) => readFileSync(path, 'utf8').replace(/\r\n/g, '\n') +const sourcePath = join(root, 'src/main/project-groups/nested-repo-discovery.ts') +const original = readSource(sourcePath) +const patch = parsePatch(readSource(join(__dirname, 'fix.patch'))) +assert.equal(patch.length, 1) +const baseline = applyPatch(original, reversePatch(patch[0])) +assert.notEqual(baseline, false, 'Source changed; review fix.patch') +const hookPoint = ' if (currentFolder.depth > options.maxDepth) {' +assert.equal(original.split(hookPoint).length, 2) +assert.equal(baseline.split(hookPoint).length, 2) +const sha256 = (text) => createHash('sha256').update(text).digest('hex') +const scratch = mkdtempSync(join(tmpdir(), 'orca-nested-queue-proof-')) +const branchCount = 96 +const rulesPerBranch = 64 +const pauseLeaf = branchCount - 2 +const tick = () => new Promise((resolve) => setImmediate(resolve)) +async function gc() { + for (let round = 0; round < 4; round++) { + await tick() + global.gc() + } +} +async function run(mode) { + const output = join(scratch, `${mode}.cjs`) + let source = mode === 'after' ? original : baseline + if (mode === 'clear-consumed-slot') { + const dequeue = ' const currentFolder = foldersToTraverse[nextFolderIndex++]' + assert.equal(source.split(dequeue).length, 2) + source = source.replace( + dequeue, + `${dequeue}\n foldersToTraverse[nextFolderIndex - 1] = undefined` + ) + } + const observedSource = source.replace( + hookPoint, + ` globalThis.__orcaObserveNestedQueue(currentFolder, foldersToTraverse, nextFolderIndex)\n${ + hookPoint + }` + ) + await build({ + entryPoints: [sourcePath], + outfile: output, + bundle: true, + platform: 'node', + format: 'cjs', + logLevel: 'silent', + plugins: [ + { + name: 'observe-actual-nested-queue', + setup(build) { + build.onLoad({ filter: /nested-repo-discovery\.ts$/ }, () => ({ + contents: observedSource, + loader: 'ts', + resolveDir: join(root, 'src/main/project-groups') + })) + build.onResolve({ filter: /^\.\.\/git\/repo$/ }, () => ({ + path: 'inert-git', + namespace: 'proof' + })) + build.onLoad({ filter: /.*/, namespace: 'proof' }, () => ({ + contents: + 'export function isGitRepo() { throw new Error("fixture must use injected filesystem") }', + loader: 'js' + })) + } + } + ] + }) + const { scanNestedRepos } = require(output) + const references = [] + const visits = [] + let pausedState + globalThis.__orcaObserveNestedQueue = (current, queue, head) => { + references.push({ + path: current.path, + record: new WeakRef(current), + inheritedRules: new WeakRef(current.ignoreRules) + }) + if (current.path === `/fixture/b${String(pauseLeaf).padStart(3, '0')}/leaf`) { + pausedState = { + allocatedSlots: queue.length, + consumedSlots: head, + pendingSlots: queue.length - head, + occupiedConsumedSlots: queue.slice(0, head).filter(Boolean).length + } + } + } + let release + const gate = new Promise((resolve) => { + release = resolve + }) + let markPaused + const paused = new Promise((resolve) => { + markPaused = resolve + }) + const resultPromise = scanNestedRepos({ + path: '/fixture', + options: { maxDepth: 3 }, + filesystem: { + async readDirectory(path) { + visits.push(path) + if (path === '/fixture') { + return Array.from({ length: branchCount }, (_, index) => ({ + name: `b${String(index).padStart(3, '0')}`, + isDirectory: true + })) + } + if (!path.endsWith('/leaf')) { + return [ + { name: '.gitignore', isDirectory: false }, + { name: 'leaf', isDirectory: true } + ] + } + if (path === `/fixture/b${String(pauseLeaf).padStart(3, '0')}/leaf`) { + markPaused() + await gate + } + return [] + }, + async readTextFile(path) { + return Array.from( + { length: rulesPerBranch }, + (_, index) => `${path.replaceAll('/', '_')}_unused_${index}` + ).join('\n') + }, + joinPath: (parent, name) => `${parent}/${name}`, + basename: (path) => path.split('/').at(-1), + hasGitMarker: () => false, + isSelectedPathGitRepo: () => false + } + }) + await paused + await gc() + const completedLeaves = references.filter( + ({ path }) => + path.endsWith('/leaf') && path !== `/fixture/b${String(pauseLeaf).padStart(3, '0')}/leaf` + ) + const retained = { + completedLeaves: completedLeaves.length, + retainedCompletedRecords: completedLeaves.filter(({ record }) => record.deref()).length, + retainedCompletedRuleArrays: completedLeaves.filter(({ inheritedRules }) => + inheritedRules.deref() + ).length + } + release() + const result = await resultPromise + delete globalThis.__orcaObserveNestedQueue + await gc() + const afterCompletion = references.filter(({ record }) => record.deref()).length + assert.equal(result.repos.length, 0) + assert.equal(result.stopped, false) + assert.equal(result.timedOut, false) + assert.equal(result.timeoutMs, null) + assert.equal(visits.length, branchCount * 2 + 1) + assert.equal(new Set(visits).size, visits.length) + assert.equal(pausedState.pendingSlots, 1) + assert.equal(retained.completedLeaves, pauseLeaf) + assert.equal(afterCompletion, 0) + delete require.cache[require.resolve(output)] + return { + mode, + pausedState, + retained, + afterCompletion, + totalVisited: visits.length, + visitedOrder: visits + } +} +async function main() { + try { + if (process.argv[2] !== '--proof-child') { + const runnerPath = join(scratch, 'run-process.cjs') + await build({ + entryPoints: [join(root, 'src/shared/child-process/run-process.ts')], + outfile: runnerPath, + bundle: true, + platform: 'node', + format: 'cjs', + logLevel: 'silent' + }) + const { runProcess } = require(runnerPath) + const child = await runProcess({ + program: process.execPath, + args: ['--expose-gc', '--max-old-space-size=256', __filename, '--proof-child'], + cwd: root, + env: process.env, + timeoutMs: 15_000, + maxOutputBytes: 1024 * 1024 + }) + assert.equal(child.timedOut, false, 'Proof timed out') + assert.equal(child.code, 0, child.stderr || child.stdout) + const recorded = JSON.parse(child.stdout) + const historical = await runProcess({ + program: 'git', + args: ['show', 'v1.4.198:src/main/project-groups/nested-repo-discovery.ts'], + cwd: root, + timeoutMs: 5_000, + maxOutputBytes: 256 * 1024 + }) + assert.equal(historical.timedOut, false) + assert.equal(historical.code, 0) + const historicalHash = sha256(historical.stdout.replace(/\r\n/g, '\n')) + assert.equal(historicalHash, recorded.sourceHashes.before) + console.log( + JSON.stringify( + { + ...recorded, + historicalSource: { ref: 'v1.4.198', sha256: historicalHash, equalsBaseline: true }, + process: { + exitCode: child.code, + timedOut: child.timedOut, + timeoutMs: 15_000, + oldSpaceMiB: 256 + } + }, + null, + 2 + ) + ) + delete require.cache[require.resolve(runnerPath)] + return + } + const before = await run('before') + const control = await run('clear-consumed-slot') + const after = await run('after') + assert.equal(before.retained.retainedCompletedRecords, pauseLeaf) + assert.equal(before.retained.retainedCompletedRuleArrays, pauseLeaf) + for (const phase of [control, after]) { + assert.equal(phase.retained.retainedCompletedRecords, 0) + assert.equal(phase.retained.retainedCompletedRuleArrays, 0) + assert.equal(phase.pausedState.occupiedConsumedSlots, 0) + assert.deepEqual(before.visitedOrder, phase.visitedOrder) + } + assert.ok(after.pausedState.allocatedSlots <= 64) + for (const phase of [before, control, after]) { + delete phase.visitedOrder + } + console.log( + JSON.stringify({ + description: + 'Actual nested-repo scan with observational dequeue hook; before reverses fix.patch and control clears only consumed slots.', + sourceHashes: { + normalization: 'UTF-8 source with CRLF line endings normalized to LF', + before: sha256(baseline), + after: sha256(original), + rules: sha256( + readSource(join(root, 'src/main/project-groups/nested-repo-scan-rules.ts')) + ), + regression: sha256( + readSource(join(root, 'src/main/project-groups/nested-repo-discovery-queue.test.ts')) + ), + runner: sha256(readSource(__filename)) + }, + nodeVersion: process.version, + branchCount, + rulesPerBranch, + before, + control, + after, + passed: true + }) + ) + } finally { + delete globalThis.__orcaObserveNestedQueue + rmSync(scratch, { recursive: true, force: true }) + } +} +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/docs/audits/nested-repo-processed-queue/results.json b/docs/audits/nested-repo-processed-queue/results.json new file mode 100644 index 00000000000..0fca2b45cee --- /dev/null +++ b/docs/audits/nested-repo-processed-queue/results.json @@ -0,0 +1,74 @@ +{ + "description": "Actual nested-repo scan with observational dequeue hook; before reverses fix.patch and control clears only consumed slots.", + "sourceHashes": { + "normalization": "UTF-8 source with CRLF line endings normalized to LF", + "before": "0a0952888db648a77776becfa9fcfc783d0b905ce096e70311d532ea2675065e", + "after": "8517a2bc2220e5fb3e96f063e1714911235ab040ee21487ca537d5c34d3cc81d", + "rules": "4613599bf5382edd86ae33bc84548f247018f26acbf2f7db072d847fa5533660", + "regression": "5e343d4da0627458ebd15c5c619b861c07388830b2a65aef76c9fb0dcd9d4c8d", + "runner": "2bafd8e2de95a86bcefdef484f63002cb0f59428b5359a3b9b9f16b8942ac11f" + }, + "nodeVersion": "v26.6.0", + "branchCount": 96, + "rulesPerBranch": 64, + "before": { + "mode": "before", + "pausedState": { + "allocatedSlots": 193, + "consumedSlots": 192, + "pendingSlots": 1, + "occupiedConsumedSlots": 192 + }, + "retained": { + "completedLeaves": 94, + "retainedCompletedRecords": 94, + "retainedCompletedRuleArrays": 94 + }, + "afterCompletion": 0, + "totalVisited": 193 + }, + "control": { + "mode": "clear-consumed-slot", + "pausedState": { + "allocatedSlots": 193, + "consumedSlots": 192, + "pendingSlots": 1, + "occupiedConsumedSlots": 0 + }, + "retained": { + "completedLeaves": 94, + "retainedCompletedRecords": 0, + "retainedCompletedRuleArrays": 0 + }, + "afterCompletion": 0, + "totalVisited": 193 + }, + "after": { + "mode": "after", + "pausedState": { + "allocatedSlots": 34, + "consumedSlots": 33, + "pendingSlots": 1, + "occupiedConsumedSlots": 0 + }, + "retained": { + "completedLeaves": 94, + "retainedCompletedRecords": 0, + "retainedCompletedRuleArrays": 0 + }, + "afterCompletion": 0, + "totalVisited": 193 + }, + "passed": true, + "historicalSource": { + "ref": "v1.4.198", + "sha256": "0a0952888db648a77776becfa9fcfc783d0b905ce096e70311d532ea2675065e", + "equalsBaseline": true + }, + "process": { + "exitCode": 0, + "timedOut": false, + "timeoutMs": 15000, + "oldSpaceMiB": 256 + } +} diff --git a/src/main/project-groups/nested-repo-discovery-queue.test.ts b/src/main/project-groups/nested-repo-discovery-queue.test.ts new file mode 100644 index 00000000000..16eb82fdd54 --- /dev/null +++ b/src/main/project-groups/nested-repo-discovery-queue.test.ts @@ -0,0 +1,122 @@ +import { posix, win32 } from 'node:path' +import { afterEach, expect, it, vi } from 'vitest' +import { scanNestedRepos } from './nested-repo-discovery' + +const branchNames = Array.from( + { length: 160 }, + (_, index) => `branch-${String(index).padStart(3, '0')}` +) +afterEach(() => vi.restoreAllMocks()) + +function fixture(paths: typeof posix, onRead: (count: number) => void = () => {}) { + const root = paths.resolve('/workspace') + const visits: string[] = [] + const branches = branchNames.map((name) => paths.join(root, name)) + const descendants = branches.map((path) => paths.join(path, 'deeper')) + const repositories = descendants.map((path) => paths.join(path, 'repository')) + return { + root, + visits, + branches, + descendants, + repositories, + filesystem: { + readDirectory: async (path: string) => { + visits.push(path) + onRead(visits.length) + const names = + path === root + ? branchNames.toReversed() + : paths.basename(path) === 'deeper' + ? ['repository'] + : ['ignored', 'deeper', '.gitignore'] + return names.map((name) => ({ name, isDirectory: name !== '.gitignore' })) + }, + readTextFile: async () => 'ignored/', + joinPath: paths.join, + basename: paths.basename, + hasGitMarker: (path: string) => paths.basename(path) === 'repository', + isSelectedPathGitRepo: () => false + } + } +} + +it.each([ + ['local Windows paths', win32], + ['SSH POSIX paths', posix] +] as const)('preserves broad BFS order and inherited ignores with %s', async (_label, paths) => { + const f = fixture(paths) + const result = await scanNestedRepos({ + path: f.root, + options: { maxRepos: 500 }, + filesystem: f.filesystem + }) + expect(f.visits).toEqual([f.root, ...f.branches, ...f.descendants]) + expect(result.repos.map(({ path }) => path)).toEqual(f.repositories) + expect(result.repos.every(({ depth }) => depth === 3)).toBe(true) + expect(result).toMatchObject({ + truncated: false, + stopped: false, + timedOut: false, + timeoutMs: null + }) +}) + +it('preserves max depth and result caps during broad traversal', async () => { + const depth = fixture(posix) + const boundedDepth = await scanNestedRepos({ + path: depth.root, + options: { maxDepth: 1 }, + filesystem: depth.filesystem + }) + expect(depth.visits).toEqual([depth.root, ...depth.branches]) + expect(boundedDepth.repos).toEqual([]) + const capped = fixture(posix) + const boundedResults = await scanNestedRepos({ + path: capped.root, + options: { maxRepos: 7 }, + filesystem: capped.filesystem + }) + expect(boundedResults.repos.map(({ path }) => path)).toEqual(capped.repositories.slice(0, 7)) + expect(boundedResults.truncated).toBe(true) +}) + +it('honors abort after a broad prefix has been consumed', async () => { + const controller = new AbortController() + const f = fixture(posix, (count) => { + if (count === 200) { + controller.abort() + } + }) + const result = await scanNestedRepos({ + path: f.root, + signal: controller.signal, + options: { maxRepos: 500 }, + filesystem: f.filesystem + }) + expect(f.visits).toHaveLength(200) + expect(result.repos.map(({ path }) => path)).toEqual(f.repositories.slice(0, 38)) + expect(result).toMatchObject({ stopped: true, timedOut: false }) +}) + +it.each([null, 500])( + 'preserves optional timeout=%s after consuming a broad prefix', + async (timeoutMs) => { + let now = 0 + vi.spyOn(Date, 'now').mockImplementation(() => now) + const f = fixture(posix, (count) => { + if (count === 200) { + now = 1_000 + } + }) + const result = await scanNestedRepos({ + path: f.root, + options: { maxRepos: 500, timeoutMs }, + filesystem: f.filesystem + }) + expect(result.repos.map(({ path }) => path)).toEqual( + timeoutMs === null ? f.repositories : f.repositories.slice(0, 38) + ) + expect(result).toMatchObject({ timedOut: timeoutMs !== null, timeoutMs, stopped: false }) + } +) diff --git a/src/main/project-groups/nested-repo-discovery.ts b/src/main/project-groups/nested-repo-discovery.ts index 84fddfd1167..1f25a85a26f 100644 --- a/src/main/project-groups/nested-repo-discovery.ts +++ b/src/main/project-groups/nested-repo-discovery.ts @@ -90,7 +90,7 @@ export async function scanNestedRepos(args: { return buildResult('non_git_folder') } - const foldersToTraverse: TraversalFolder[] = [ + const foldersToTraverse: (TraversalFolder | undefined)[] = [ { path: args.path, depth: 0, segments: [], ignoreRules: [] } ] let nextFolderIndex = 0 @@ -107,7 +107,13 @@ export async function scanNestedRepos(args: { if (noteAbort()) { break } - const currentFolder = foldersToTraverse[nextFolderIndex++] + const currentFolder = foldersToTraverse[nextFolderIndex++]! + // Release processed paths and inherited ignore rules before the next filesystem await. + foldersToTraverse[nextFolderIndex - 1] = undefined + if (nextFolderIndex >= 64 && nextFolderIndex * 2 >= foldersToTraverse.length) { + foldersToTraverse.splice(0, nextFolderIndex) + nextFolderIndex = 0 + } if (currentFolder.depth > options.maxDepth) { continue } From c3c051dfa62860e0eec6dfc37b81106eba394125 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:32:45 -0700 Subject: [PATCH 32/59] Release provider children after structured session holds disappear (#20978) * fix(chat): release provider children after lost resume holds * test: load audit fixtures as modules and verify combined mobile payload --------- Co-authored-by: m4air --- .../structured-hold-retention/README.md | 51 ++++ .../structured-hold-retention/reproduce.mjs | 129 ++++++++ .../structured-hold-retention/results.json | 18 ++ ...red-agent-session-hold-resume-race.test.ts | 289 ++++++++++++++++++ .../structured-agent-session-holders.ts | 28 +- .../structured-agent-session-holds.ts | 17 +- .../structured-agent-session-hold.test.ts | 101 ++++++ .../methods/structured-agent-session-hold.ts | 4 +- ...ss-version-agent-session-wire.unit.test.ts | 20 +- 9 files changed, 631 insertions(+), 26 deletions(-) create mode 100644 docs/audits/structured-hold-retention/README.md create mode 100644 docs/audits/structured-hold-retention/reproduce.mjs create mode 100644 docs/audits/structured-hold-retention/results.json create mode 100644 src/main/native-chat/agent-session-wire/structured-agent-session-hold-resume-race.test.ts diff --git a/docs/audits/structured-hold-retention/README.md b/docs/audits/structured-hold-retention/README.md new file mode 100644 index 00000000000..c0076813f6c --- /dev/null +++ b/docs/audits/structured-hold-retention/README.md @@ -0,0 +1,51 @@ +# Structured session hold lost during resume + +Run from the repository root: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node docs/audits/structured-hold-retention/reproduce.mjs +``` + +The script bundles the actual `StructuredAgentSessionHolds` implementation into temporary CommonJS +modules, loads them normally, and removes their files and module-cache entries. It runs the code +once without the post-resume holder check and once with the current source. It uses a deferred +provider acquisition, an isolated fake child, and a 5 ms release grace. It launches no application, +provider, or terminal and reads no user profile. + +The last surface releases its hold while acquisition is pending. At that point the session has no +provider child, so `release()` cannot arm the release clock. Before the fix, acquisition completes +with a child, zero holders, and no scheduled eviction. With the fix, successful acquisition checks +for surviving holders and schedules the existing release clock. The recorded child is released once. + +The RPC path registers connection cleanup before awaiting `host.hold()` in +`src/main/runtime/rpc/methods/structured-agent-session-hold.ts`. Runtime socket close calls +`cleanupSubscriptionsForConnection()` in `runtime-rpc/runtime-rpc-lifecycle.ts`. That supplies the +production release-during-acquisition ordering reproduced here. + +## Ownership limits + +- This proves a lifecycle race, not that it caused any particular OOM report. No process RSS was + measured. It applies to structured sessions acquiring a provider child, not ordinary PTY tabs. +- The release clock preserves its 15-second production grace, waits while a turn is active, and + cancels when a new holder arrives. Acquisition failures retain their existing handling. +- Disposal prevents late acquisition or release callbacks from restarting the clock. Host teardown + owns cleanup after disposal. The host's broader pre-attach shutdown admission is outside this fix. +- A restored childless journal is not necessarily abandoned. Startup selects persisted visible + tabs; `host.sessions` supplies `listSessionTabs()`, and childless sessions can retain live TUI + owners. `host.close()` closes that TUI owner before removing the journal. This fix neither evicts + childless history nor infers process exit from transport loss. +- Execution remains on the owning runtime, with no wire or SSH routing changes. + +Targeted regressions live in +`src/main/native-chat/agent-session-wire/structured-agent-session-hold-resume-race.test.ts`. +They cover last-holder loss, active turns, new holders, reconnection, failed acquisition, explicit +close, and disposal. + +Same-ID replacement is fenced at both ownership layers. Holder entries receive a new incarnation +after release and re-add, so an old failed acquisition cannot remove a replacement. The RPC uses +the subscription registry's `releaseIfCurrent()` cleanup, so its failure cannot unregister the +replacement's connection cleanup. Duplicate adds remain one holder. Class tests and real host/RPC +tests cover the old failure arriving before and after replacement success; disconnect still releases +the replacement normally. They also cover the reverse outcome: an old acquisition succeeds and the +replacement refuses a stale fence. Its last-holder rollback starts the same turn-aware release clock +for the acquired child. No RPC fields or published frame shapes change. diff --git a/docs/audits/structured-hold-retention/reproduce.mjs b/docs/audits/structured-hold-retention/reproduce.mjs new file mode 100644 index 00000000000..b1e6ed5c32c --- /dev/null +++ b/docs/audits/structured-hold-retention/reproduce.mjs @@ -0,0 +1,129 @@ +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 } from 'node:path' +import { fileURLToPath } 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 sourcePath = fileURLToPath( + new URL( + '../../../src/main/native-chat/agent-session-wire/structured-agent-session-holds.ts', + import.meta.url + ) +) +const source = await readFile(sourcePath, 'utf8') +const postResumeCheck = + ' // The last surface can disconnect before acquisition makes a child available to release.\n' + + ' if (!this.disposed && !this.holders.isHeld(sessionId)) {\n' + + ' this.clock.arm(sessionId)\n' + + ' }\n' +if (!source.includes(postResumeCheck)) { + throw new Error('Source changed: review the before-fix transform before running this proof.') +} + +async function loadHolds(withPostResumeCheck) { + const result = await build({ + absWorkingDir: root, + entryPoints: [sourcePath], + bundle: true, + platform: 'node', + format: 'cjs', + write: false, + logLevel: 'silent', + plugins: [ + { + name: 'compare-post-resume-holder-check', + setup(plugin) { + plugin.onLoad({ filter: /structured-agent-session-holds\.ts$/ }, () => ({ + contents: withPostResumeCheck ? source : source.replace(postResumeCheck, ''), + loader: 'ts' + })) + } + } + ] + }) + const scratch = await mkdtemp(join(tmpdir(), 'orca-structured-hold-proof-')) + const require = createRequire(import.meta.url) + let moduleId + try { + const bundlePath = join(scratch, 'holds.cjs') + await writeFile(bundlePath, result.outputFiles[0].text) + moduleId = require.resolve(bundlePath) + return require(moduleId).StructuredAgentSessionHolds + } finally { + if (moduleId) { + delete require.cache[moduleId] + } + await rm(scratch, { recursive: true, force: true }) + } +} + +async function reproduce(Holds) { + const gate = Promise.withResolvers() + let child = false + let evictions = 0 + const holds = new Holds({ + resume: async () => { + await gate.promise + child = true + }, + hasProviderChild: () => child, + isTurnActive: () => false, + evict: async () => { + evictions += 1 + child = false + }, + graceMs: 5 + }) + try { + const acquiring = holds.hold('restored-session', 'connection:surface') + holds.release('restored-session', 'connection:surface') + gate.resolve() + await acquiring + const releasePendingAfterAcquisition = holds.isReleasePending('restored-session') + await new Promise((resolve) => setTimeout(resolve, 30)) + return { + child, + held: holds.isHeld('restored-session'), + releasePendingAfterAcquisition, + evictions + } + } finally { + holds.dispose() + } +} + +const before = await reproduce(await loadHolds(false)) +const after = await reproduce(await loadHolds(true)) +const passed = + before.child && + !before.held && + !before.releasePendingAfterAcquisition && + before.evictions === 0 && + !after.child && + !after.held && + after.releasePendingAfterAcquisition && + after.evictions === 1 +console.log( + JSON.stringify( + { + source: 'src/main/native-chat/agent-session-wire/structured-agent-session-holds.ts', + sourceSha256: createHash('sha256').update(source).digest('hex'), + comparison: 'same source, before omits only the post-resume holder check', + before, + after, + passed + }, + null, + 2 + ) +) +if (!passed) { + process.exitCode = 1 +} diff --git a/docs/audits/structured-hold-retention/results.json b/docs/audits/structured-hold-retention/results.json new file mode 100644 index 00000000000..6019410f918 --- /dev/null +++ b/docs/audits/structured-hold-retention/results.json @@ -0,0 +1,18 @@ +{ + "source": "src/main/native-chat/agent-session-wire/structured-agent-session-holds.ts", + "sourceSha256": "c95e27518d6cb09f1c97e5ff18bbb9fc5b15c7680d4a99e90cc14353d602230f", + "comparison": "same source, before omits only the post-resume holder check", + "before": { + "child": true, + "held": false, + "releasePendingAfterAcquisition": false, + "evictions": 0 + }, + "after": { + "child": false, + "held": false, + "releasePendingAfterAcquisition": true, + "evictions": 1 + }, + "passed": true +} diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-hold-resume-race.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-hold-resume-race.test.ts new file mode 100644 index 00000000000..bd3a840d9ad --- /dev/null +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-hold-resume-race.test.ts @@ -0,0 +1,289 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { StructuredAgentSessionHolds } from './structured-agent-session-holds' + +const GRACE_MS = 15_000 +const pendingHolds: StructuredAgentSessionHolds[] = [] + +function resumeHarness() { + const resumeGate = Promise.withResolvers() + let child = false + let turnActive = false + const evict = vi.fn(async () => { + child = false + }) + const holds = new StructuredAgentSessionHolds({ + resume: async () => { + await resumeGate.promise + child = true + }, + hasProviderChild: () => child, + isTurnActive: () => turnActive, + evict, + graceMs: GRACE_MS + }) + pendingHolds.push(holds) + return { + holds, + resumeGate, + evict, + hasChild: () => child, + setTurnActive: (value: boolean) => { + turnActive = value + } + } +} + +beforeEach(() => vi.useFakeTimers()) +afterEach(() => { + for (const holds of pendingHolds.splice(0)) { + holds.dispose() + } + vi.useRealTimers() +}) + +describe('a surface leaving while its structured session resumes', () => { + it('releases the acquired child after the last surface disconnects during resume', async () => { + const { holds, resumeGate, evict, hasChild } = resumeHarness() + const hold = holds.hold('session-1', 'connection-1:chat') + + holds.release('session-1', 'connection-1:chat') + expect(holds.isReleasePending('session-1')).toBe(false) + resumeGate.resolve() + await hold + + expect(hasChild()).toBe(true) + expect(holds.isHeld('session-1')).toBe(false) + expect(holds.isReleasePending('session-1')).toBe(true) + await vi.advanceTimersByTimeAsync(GRACE_MS - 1) + expect(evict).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(1) + expect(evict).toHaveBeenCalledExactlyOnceWith('session-1') + expect(hasChild()).toBe(false) + }) + + it('waits for an active turn before releasing the late child', async () => { + const { holds, resumeGate, evict, setTurnActive } = resumeHarness() + const hold = holds.hold('session-1', 'connection-1:chat') + holds.release('session-1', 'connection-1:chat') + setTurnActive(true) + resumeGate.resolve() + await hold + + await vi.advanceTimersByTimeAsync(GRACE_MS * 2) + expect(evict).not.toHaveBeenCalled() + expect(holds.isReleasePending('session-1')).toBe(true) + + setTurnActive(false) + await vi.advanceTimersByTimeAsync(GRACE_MS) + expect(evict).toHaveBeenCalledExactlyOnceWith('session-1') + }) + + it.each([false, true])('preserves an arriving holder with resume=%s', async (resume) => { + const { holds, resumeGate, evict } = resumeHarness() + const first = holds.hold('session-1', 'connection-1:chat') + holds.release('session-1', 'connection-1:chat') + const replacement = holds.hold('session-1', 'connection-2:chat', { resume }) + resumeGate.resolve() + await Promise.all([first, replacement]) + + await vi.advanceTimersByTimeAsync(GRACE_MS * 2) + expect(holds.isHeld('session-1')).toBe(true) + expect(holds.isReleasePending('session-1')).toBe(false) + expect(evict).not.toHaveBeenCalled() + + holds.release('session-1', 'connection-2:chat') + await vi.advanceTimersByTimeAsync(GRACE_MS) + expect(evict).toHaveBeenCalledExactlyOnceWith('session-1') + }) + + it('cancels the late-child release when a surface reconnects during grace', async () => { + const { holds, resumeGate, evict } = resumeHarness() + const hold = holds.hold('session-1', 'connection-1:chat') + holds.release('session-1', 'connection-1:chat') + resumeGate.resolve() + await hold + expect(holds.isReleasePending('session-1')).toBe(true) + + await holds.hold('session-1', 'connection-2:chat') + await vi.advanceTimersByTimeAsync(GRACE_MS * 2) + expect(holds.isReleasePending('session-1')).toBe(false) + expect(evict).not.toHaveBeenCalled() + }) + + it('preserves a failed resume without scheduling eviction', async () => { + const { holds, resumeGate, evict, hasChild } = resumeHarness() + const failure = new Error('provider acquisition failed') + const hold = holds.hold('session-1', 'connection-1:chat') + const rejected = expect(hold).rejects.toBe(failure) + holds.release('session-1', 'connection-1:chat') + resumeGate.reject(failure) + await rejected + + await vi.advanceTimersByTimeAsync(GRACE_MS * 2) + expect(hasChild()).toBe(false) + expect(holds.isHeld('session-1')).toBe(false) + expect(holds.isReleasePending('session-1')).toBe(false) + expect(evict).not.toHaveBeenCalled() + }) + + it('leaves late acquisition cleanup to host teardown after disposal', async () => { + const { holds, resumeGate, evict } = resumeHarness() + const hold = holds.hold('session-1', 'connection-1:chat') + holds.release('session-1', 'connection-1:chat') + holds.dispose() + resumeGate.resolve() + await hold + + await vi.advanceTimersByTimeAsync(GRACE_MS * 2) + expect(holds.isReleasePending('session-1')).toBe(false) + expect(evict).not.toHaveBeenCalled() + }) + + it('does not restart release timers when a surface leaves after disposal', async () => { + const { holds, resumeGate, evict } = resumeHarness() + const hold = holds.hold('session-1', 'connection-1:chat') + resumeGate.resolve() + await hold + holds.dispose() + holds.release('session-1', 'connection-1:chat') + + await vi.advanceTimersByTimeAsync(GRACE_MS * 2) + expect(holds.isHeld('session-1')).toBe(false) + expect(holds.isReleasePending('session-1')).toBe(false) + expect(evict).not.toHaveBeenCalled() + }) + + it('releases a late child acquired after explicit close forgot its holders', async () => { + const { holds, resumeGate, evict } = resumeHarness() + const hold = holds.hold('session-1', 'connection-1:chat') + holds.forget('session-1') + resumeGate.resolve() + await hold + + await vi.advanceTimersByTimeAsync(GRACE_MS) + expect(holds.isHeld('session-1')).toBe(false) + expect(evict).toHaveBeenCalledExactlyOnceWith('session-1') + }) + + it.each([false, true])( + 'keeps a reused holder when old resume fails (replacement finished=%s)', + async (replacementFinished) => { + const firstGate = Promise.withResolvers() + const replacementGate = Promise.withResolvers() + let child = false + const resume = vi + .fn() + .mockImplementationOnce(() => firstGate.promise) + .mockImplementationOnce(async () => { + await replacementGate.promise + child = true + }) + const evict = vi.fn(async () => {}) + const holds = new StructuredAgentSessionHolds({ + resume, + hasProviderChild: () => child, + isTurnActive: () => false, + evict, + graceMs: GRACE_MS + }) + pendingHolds.push(holds) + const first = holds.hold('session-1', 'same-holder') + const rejected = expect(first).rejects.toThrow('old acquisition failed') + holds.release('session-1', 'same-holder') + const replacement = holds.hold('session-1', 'same-holder') + if (replacementFinished) { + replacementGate.resolve() + await replacement + } + + firstGate.reject(new Error('old acquisition failed')) + await rejected + expect(holds.isHeld('session-1')).toBe(true) + replacementGate.resolve() + await replacement + await vi.advanceTimersByTimeAsync(GRACE_MS * 2) + expect(evict).not.toHaveBeenCalled() + + holds.release('session-1', 'same-holder') + await vi.advanceTimersByTimeAsync(GRACE_MS) + expect(evict).toHaveBeenCalledExactlyOnceWith('session-1') + } + ) + + it('removes a failed replacement while the released old hold is still pending', async () => { + const firstGate = Promise.withResolvers() + const resume = vi + .fn() + .mockImplementationOnce(() => firstGate.promise) + .mockRejectedValueOnce(new Error('replacement acquisition failed')) + const holds = new StructuredAgentSessionHolds({ + resume, + hasProviderChild: () => false, + isTurnActive: () => false, + evict: async () => {}, + graceMs: GRACE_MS + }) + pendingHolds.push(holds) + const first = holds.hold('session-1', 'same-holder') + const rejected = expect(first).rejects.toThrow('old acquisition failed') + holds.release('session-1', 'same-holder') + + await expect(holds.hold('session-1', 'same-holder')).rejects.toThrow( + 'replacement acquisition failed' + ) + expect(holds.isHeld('session-1')).toBe(false) + expect(holds.isReleasePending('session-1')).toBe(false) + + firstGate.reject(new Error('old acquisition failed')) + await rejected + expect(holds.isHeld('session-1')).toBe(false) + }) + + it.each(['old-holder', 'different-holder'])( + 'releases the old acquisition after replacement %s fails, once its turn finishes', + async (replacementHolder) => { + const firstGate = Promise.withResolvers() + const replacementGate = Promise.withResolvers() + let child = false + let turnActive = true + const resume = vi + .fn() + .mockImplementationOnce(async () => { + await firstGate.promise + child = true + }) + .mockImplementationOnce(() => replacementGate.promise) + const evict = vi.fn(async () => { + child = false + }) + const holds = new StructuredAgentSessionHolds({ + resume, + hasProviderChild: () => child, + isTurnActive: () => turnActive, + evict, + graceMs: GRACE_MS + }) + pendingHolds.push(holds) + const first = holds.hold('session-1', 'old-holder') + holds.release('session-1', 'old-holder') + const replacement = holds.hold('session-1', replacementHolder) + const rejected = expect(replacement).rejects.toThrow('replacement acquisition failed') + firstGate.resolve() + await first + expect(holds.isReleasePending('session-1')).toBe(false) + + replacementGate.reject(new Error('replacement acquisition failed')) + await rejected + expect(holds.isHeld('session-1')).toBe(false) + expect(holds.isReleasePending('session-1')).toBe(true) + await vi.advanceTimersByTimeAsync(GRACE_MS) + expect(evict).not.toHaveBeenCalled() + expect(child).toBe(true) + + turnActive = false + await vi.advanceTimersByTimeAsync(GRACE_MS) + expect(evict).toHaveBeenCalledExactlyOnceWith('session-1') + expect(child).toBe(false) + } + ) +}) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-holders.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-holders.ts index ed0451efb79..531717168b1 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-holders.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-holders.ts @@ -6,23 +6,35 @@ // still looking at, and a lost one leaks the child forever. A set answers both idempotently, // because it records WHICH surface holds the session, not how many do. +type Holder = { resumeCapable: boolean; incarnation: symbol } + export class StructuredAgentSessionHolders { - private readonly bySession = new Map>() + private readonly bySession = new Map>() /** True when the session gained its FIRST holder — the edge that ends a pending release. */ add(sessionId: string, holderId: string, resumeCapable = true): boolean { const holders = this.bySession.get(sessionId) if (!holders) { - this.bySession.set(sessionId, new Map([[holderId, resumeCapable]])) + this.bySession.set(sessionId, new Map([[holderId, { resumeCapable, incarnation: Symbol() }]])) return true } - holders.set(holderId, (holders.get(holderId) ?? false) || resumeCapable) + const previous = holders.get(holderId) + holders.set(holderId, { + resumeCapable: (previous?.resumeCapable ?? false) || resumeCapable, + incarnation: previous?.incarnation ?? Symbol() + }) return false } /** True when the session lost its LAST holder — the edge that starts one. */ - remove(sessionId: string, holderId: string): boolean { + remove(sessionId: string, holderId: string, expectedIncarnation?: symbol): boolean { const holders = this.bySession.get(sessionId) + if ( + expectedIncarnation !== undefined && + holders?.get(holderId)?.incarnation !== expectedIncarnation + ) { + return false + } if (!holders?.delete(holderId) || holders.size > 0) { return false } @@ -38,12 +50,18 @@ export class StructuredAgentSessionHolders { return this.bySession.get(sessionId)?.has(holderId) ?? false } + incarnation(sessionId: string, holderId: string): symbol | undefined { + return this.bySession.get(sessionId)?.get(holderId)?.incarnation + } + holderIds(sessionId: string): string[] { return [...(this.bySession.get(sessionId)?.keys() ?? [])] } hasResumeCapableHolder(sessionId: string): boolean { - return [...(this.bySession.get(sessionId)?.values() ?? [])].some(Boolean) + return [...(this.bySession.get(sessionId)?.values() ?? [])].some( + (holder) => holder.resumeCapable + ) } /** Drops every holder of one session without evaluating the edge, for a session that is gone. */ diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-holds.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-holds.ts index 6afc8abc052..027c2d54772 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-holds.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-holds.ts @@ -36,6 +36,7 @@ export type StructuredAgentSessionHoldOptions = { export class StructuredAgentSessionHolds { private readonly holders = new StructuredAgentSessionHolders() private readonly clock: StructuredAgentSessionReleaseClock + private disposed = false constructor(private readonly deps: StructuredAgentSessionHoldsDeps) { const clockDeps: StructuredAgentSessionReleaseClockDeps = { @@ -55,6 +56,7 @@ export class StructuredAgentSessionHolds { ): Promise { const alreadyHeld = this.holders.has(sessionId, holderId) this.holders.add(sessionId, holderId, options.resume !== false) + const incarnation = this.holders.incarnation(sessionId, holderId) // Unconditional, not only on the first-holder edge: a second surface arriving during the grace // window must cancel the pending release too. this.clock.cancel(sessionId) @@ -66,19 +68,23 @@ export class StructuredAgentSessionHolds { if (!this.deps.hasProviderChild(sessionId)) { throw new Error('agent_session_ownership_unknown') } + // The last surface can disconnect before acquisition makes a child available to release. + if (!this.disposed && !this.holders.isHeld(sessionId)) { + this.clock.arm(sessionId) + } } catch (error) { - if (!alreadyHeld) { - this.holders.remove(sessionId, holderId) + if (!alreadyHeld && incarnation !== undefined) { + this.release(sessionId, holderId, incarnation) } throw error } } - release(sessionId: string, holderId: string): void { - if (!this.holders.remove(sessionId, holderId)) { + release(sessionId: string, holderId: string, expectedIncarnation?: symbol): void { + if (!this.holders.remove(sessionId, holderId, expectedIncarnation)) { return } - if (this.deps.hasProviderChild(sessionId)) { + if (!this.disposed && this.deps.hasProviderChild(sessionId)) { this.clock.arm(sessionId) } } @@ -102,6 +108,7 @@ export class StructuredAgentSessionHolds { } dispose(): void { + this.disposed = true this.clock.dispose() } } diff --git a/src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts index 4e6dfdf45bf..da34108604a 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts @@ -162,6 +162,107 @@ describe('a client that holds a session', () => { }) describe('a client that disappears without cleanup', () => { + it('releases a late child after its same-ID replacement refuses the stale fence', async () => { + await host.close(SESSION) + await host.restoreReadableSessions() + closeSession.mockClear() + const firstEntered = Promise.withResolvers() + const firstGate = Promise.withResolvers() + const replacementEntered = Promise.withResolvers() + const replacementGate = Promise.withResolvers() + const attach = host.attach.bind(host) + const attachSpy = vi + .spyOn(host, 'attach') + .mockImplementationOnce(async (...args) => { + firstEntered.resolve() + await firstGate.promise + return attach(...args) + }) + .mockImplementationOnce(async (...args) => { + replacementEntered.resolve() + await replacementGate.promise + return attach(...args) + }) + try { + const params = { sessionId: SESSION, holderId: 'same-chat' } + const first = call('agentSession.hold', params) + await firstEntered.promise + const replacement = call('agentSession.hold', params) + await replacementEntered.promise + firstGate.resolve() + expect(await first).toMatchObject({ ok: true }) + expect(host.isHeld(SESSION)).toBe(true) + expect(closeSession).not.toHaveBeenCalled() + + replacementGate.resolve() + expect(await replacement).toMatchObject({ + ok: false, + error: { code: 'agent_session_checkpoint_stale' } + }) + expect(host.isHeld(SESSION)).toBe(false) + await vi.waitFor(() => expect(host.hasSession(SESSION)).toBe(false)) + expect(closeSession).toHaveBeenCalledExactlyOnceWith(SESSION) + } finally { + firstGate.resolve() + replacementGate.resolve() + attachSpy.mockRestore() + } + }) + + it.each([false, true])( + 'keeps replacement hold and cleanup after an old request fails (replacement finished=%s)', + async (replacementFinished) => { + await host.close(SESSION) + await host.restoreReadableSessions() + closeSession.mockClear() + const firstEntered = Promise.withResolvers() + const firstGate = Promise.withResolvers() + const replacementEntered = Promise.withResolvers() + const replacementGate = Promise.withResolvers() + const attach = host.attach.bind(host) + const attachSpy = vi + .spyOn(host, 'attach') + .mockImplementationOnce(async () => { + firstEntered.resolve() + await firstGate.promise + throw new Error('old acquisition failed') + }) + .mockImplementationOnce(async (...args) => { + replacementEntered.resolve() + await replacementGate.promise + return attach(...args) + }) + try { + const params = { sessionId: SESSION, holderId: 'same-chat' } + const first = call('agentSession.hold', params) + await firstEntered.promise + const replacement = call('agentSession.hold', params) + await replacementEntered.promise + if (replacementFinished) { + replacementGate.resolve() + expect(await replacement).toMatchObject({ ok: true }) + } + + firstGate.resolve() + expect(await first).toMatchObject({ ok: false }) + expect(host.isHeld(SESSION)).toBe(true) + replacementGate.resolve() + expect(await replacement).toMatchObject({ ok: true }) + await new Promise((resolve) => setTimeout(resolve, GRACE_MS * 4)) + expect(host.hasSession(SESSION)).toBe(true) + expect(closeSession).not.toHaveBeenCalled() + + runtime.cleanupSubscriptionsForConnection(CONNECTION) + await vi.waitFor(() => expect(host.hasSession(SESSION)).toBe(false)) + expect(closeSession).toHaveBeenCalledExactlyOnceWith(SESSION) + } finally { + firstGate.resolve() + replacementGate.resolve() + attachSpy.mockRestore() + } + } + ) + it('still releases the session when its transport closes', async () => { await call('agentSession.hold', { sessionId: SESSION, holderId: 'chat-1' }) diff --git a/src/main/runtime/rpc/methods/structured-agent-session-hold.ts b/src/main/runtime/rpc/methods/structured-agent-session-hold.ts index 18fb600a949..54753fcb889 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-hold.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-hold.ts @@ -36,7 +36,7 @@ export const STRUCTURED_AGENT_SESSION_HOLD_METHODS = [ await ensureStructuredHostInstalled(ctx) const host = requireStructuredHost(ctx) const holderKey = holderKeyFor(ctx, params.holderId) - ctx.runtime.registerSubscriptionCleanup( + const registration = ctx.runtime.registerOwnedSubscriptionCleanup( holdCleanupIdFor(params.sessionId, holderKey), () => host.release(params.sessionId, holderKey), ctx.connectionId @@ -44,7 +44,7 @@ export const STRUCTURED_AGENT_SESSION_HOLD_METHODS = [ try { await host.hold(params.sessionId, holderKey) } catch (error) { - ctx.runtime.cleanupSubscription(holdCleanupIdFor(params.sessionId, holderKey)) + registration.releaseIfCurrent() throw error } return { held: true as const } diff --git a/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts b/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts index 06b2a3f9248..a99e3812776 100644 --- a/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts +++ b/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts @@ -19,6 +19,7 @@ import type { StructuredAgentSessionAdapter } from '../../../src/main/native-cha import { StructuredAgentSessionHost } from '../../../src/main/native-chat/agent-session-wire/structured-agent-session-host' import { setStructuredAgentSessionHost } from '../../../src/main/native-chat/agent-session-wire/structured-agent-session-registry' import { AgentSessionRecordStore } from '../../../src/main/runtime/agent-session-record-store' +import { RuntimeSubscriptionRegistry } from '../../../src/main/runtime/runtime-subscription-registry' import type { AgentSessionSubscribeEvent } from '../../../src/shared/agent-session-wire' import { AGENT_SESSION_PENDING_SEND_RESULT_RUNTIME_CAPABILITY, @@ -70,7 +71,7 @@ beforeAll(async () => { }, SUITE_TIMEOUT_MS) function runtimeStub(): unknown { - const cleanups = new Map void>() + const subscriptions = new RuntimeSubscriptionRegistry() return { getRuntimeId: () => 'runtime-1', getClientSettings: () => ({ experimentalStructuredNativeChat: true }), @@ -85,19 +86,10 @@ function runtimeStub(): unknown { return resolved }, publishStructuredAgentSessionTab: () => {}, - registerSubscriptionCleanup: (id: string, cleanup: () => void) => cleanups.set(id, cleanup), - cleanupSubscription: (id: string) => { - cleanups.get(id)?.() - cleanups.delete(id) - }, - cleanupSubscriptionsByPrefix: (prefix: string) => { - for (const [id, cleanup] of cleanups) { - if (id.startsWith(prefix)) { - cleanup() - cleanups.delete(id) - } - } - } + registerSubscriptionCleanup: subscriptions.register.bind(subscriptions), + registerOwnedSubscriptionCleanup: subscriptions.registerOwned.bind(subscriptions), + cleanupSubscription: subscriptions.cleanup.bind(subscriptions), + cleanupSubscriptionsByPrefix: subscriptions.cleanupByPrefix.bind(subscriptions) } } From 1aadf9115346b28aa9acee93430e1fd47703b303 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:32:49 -0700 Subject: [PATCH 33/59] fix(runtime): preserve observed exit during explicit terminal close (#21019) * fix(pty): reconcile daemon exits after synthetic notifications * fix(runtime): preserve observed exit during explicit terminal close --------- Co-authored-by: m4air --- .../terminal-close-observed-exit/README.md | 37 ++ .../reproduce.mjs | 132 +++++++ .../terminal-close-observed-exit/results.json | 359 ++++++++++++++++++ ...runtime-stop-explicitly-closed-tab-ptys.ts | 10 + ...rminal-close-observed-exit-test-fixture.ts | 158 ++++++++ .../terminal-close-observed-exit.test.ts | 61 +++ 6 files changed, 757 insertions(+) create mode 100644 docs/audits/terminal-close-observed-exit/README.md create mode 100644 docs/audits/terminal-close-observed-exit/reproduce.mjs create mode 100644 docs/audits/terminal-close-observed-exit/results.json create mode 100644 src/main/runtime/terminal-close-observed-exit-test-fixture.ts create mode 100644 src/main/runtime/terminal-close-observed-exit.test.ts diff --git a/docs/audits/terminal-close-observed-exit/README.md b/docs/audits/terminal-close-observed-exit/README.md new file mode 100644 index 00000000000..f4d92f8ac55 --- /dev/null +++ b/docs/audits/terminal-close-observed-exit/README.md @@ -0,0 +1,37 @@ +# Preserve an observed exit during explicit terminal close + +An explicit close can receive the target daemon's physical EXIT, then fail its aggregate verification because another preserved daemon is unavailable. The close method used to invoke the fallback kill even though the runtime already held an `exited` verdict. That redundant request emitted a synthetic `-1`, replacing `operator_close` with `unknown/stop_unverified` and sending a second renderer exit notification. + +The fix captures the stamped PTY incarnation before awaiting the stop. A false stop result is accepted only when the same incarnation remains current and the runtime already has an `exited` verdict. It does not create an exit certificate from an empty inventory or transport failure. + +## Reproduce + +From the checkout, with dependencies already installed: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node docs/audits/terminal-close-observed-exit/reproduce.mjs /tmp/terminal-close-observed-exit.json +ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/runtime/terminal-close-observed-exit.test.ts +``` + +The script runs eight scenarios before and after the change, reversing only the new capture and guard for the before variant. It uses the actual runtime close method, runtime controller, daemon router, and two real daemon socket endpoints. The subprocess itself is controlled by the existing test harness. The script uses temporary configuration files, checks the expected outcomes, records source hashes, and removes its temporary directory. It does not install dependencies, launch a UI, or alter the checkout. `results.json` preserves the recorded result; use a separate output path when rerunning. + +| Scenario | Before | After | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Target physical EXIT received; unrelated daemon unavailable | Close returns false; one redundant kill; cause overwritten; two renderer exit notifications | Close returns true; no redundant kill; `operator_close` preserved; one renderer exit notification | +| Healthy aggregate inventory, delayed physical EXIT | Close succeeds | Unchanged | +| Target socket paused; unrelated daemon unavailable | Close remains unverifiable despite target's empty inventory | Unchanged | +| Same stamped incarnation already exited | Redundant fallback kill | Existing exit accepted | +| Same raw ID registered with a newer incarnation | Old certificate rejected | Unchanged | +| Synthetic negative exit, no host exit certificate | Close remains unverifiable | Unchanged | +| Unstamped legacy session | Certificate not reused | Unchanged | +| Stop throws after exit | Catch records unverifiable | Unchanged | + +In all socket scenarios, the physical provider event and runtime exit listener settle once. The fixed observed-exit case has no headless model or title tracker retained. This proof measures lifecycle behavior, not retained heap bytes. + +## Dependency and incident limits + +This change is stacked on [#21000](https://github.com/stablyai/orca/pull/21000), branch `np-oom-scan-daemon-late-exit`, and reuses its actual daemon socket fixture and late physical-exit reconciliation. #21000 fixes final DATA arriving after a synthetic exit. This change prevents a redundant synthetic exit after a physical exit has already been accepted. The before variant is the current checkout with this narrow guard reversed, not a pristine historical build. + +The unconditional fallback and exit-cause assignment are present in the reported `v1.4.197` source (`orca-runtime-stop-explicitly-closed-tab-ptys.ts`, `orca-runtime-on-pty-exit.ts`). They explain a concrete way to get a failed close and `stop_unverified` despite a confirmed local exit. [#19018](https://github.com/stablyai/orca/issues/19018) does not establish that an unrelated preserved daemon was unavailable; this is a conditional explanation, not proof of the reporter's exact ordering. + +Generic inventory remains fail-closed. Exact-owner verification across daemon generations is separate work. This change does not solve a thrown stop, SSH loss of contact, unstamped identities, or all same-ID shutdown races. A missing diagnostics row remains insufficient evidence of process death. diff --git a/docs/audits/terminal-close-observed-exit/reproduce.mjs b/docs/audits/terminal-close-observed-exit/reproduce.mjs new file mode 100644 index 00000000000..ce8873d098d --- /dev/null +++ b/docs/audits/terminal-close-observed-exit/reproduce.mjs @@ -0,0 +1,132 @@ +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 = 'src/main/runtime/orca-runtime-stop-explicitly-closed-tab-ptys.ts' +const fixturePath = 'src/main/runtime/terminal-close-observed-exit-test-fixture.ts' +const source = await readFile(join(root, sourcePath), 'utf8') +const capture = ' const expectedIncarnationId = this.ptysById.get(ptyId)?.incarnationId\n' +const guard = ` // Preserve an observed exit when a broader inventory check could not finish. + if ( + !stopped && + expectedIncarnationId && + this.ptysById.get(ptyId)?.incarnationId === expectedIncarnationId && + this.getPtyLivenessVerdict(ptyId)?.status === 'exited' + ) { + stopped = true + } +` +assert(source.includes(capture) && source.includes(guard), 'Review the baseline transform.') +const baseline = source.replace(capture, '').replace(guard, '') +const scratch = await mkdtemp(join(tmpdir(), 'orca-observed-exit-proof-')) +const phases = [] +try { + for (const phase of ['before', 'after']) { + const testPath = join(scratch, `${phase}.test.ts`) + const outputPath = join(scratch, `${phase}.json`) + 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 { runObservedExitSocketScenario, runObservedExitControl } from ${JSON.stringify(join(root, fixturePath))} +const sockets = [] +const controls = [] +for (const scenario of ['healthy', 'unrelated-endpoint-gone', 'physical-exit-observed']) { + it(scenario, async () => sockets.push(await runObservedExitSocketScenario(scenario))) +} +for (const control of ['same-incarnation', 'replacement', 'unverified', 'legacy-unstamped', 'throw-after-exit']) { + it(control, async () => controls.push(await runObservedExitControl(control))) +} +afterAll(() => writeFileSync(${JSON.stringify(outputPath)}, JSON.stringify({ sockets, controls }))) +` + ) + await writeFile( + configPath, + ` +import base from ${JSON.stringify(pathToFileURL(join(root, 'config/vitest.config.ts')).href)} +export default { + ...base, + plugins: [{ name: 'observed-exit-baseline', enforce: 'pre', transform(code, id) { + if (id.replaceAll('\\\\', '/').endsWith(${JSON.stringify(`/${sourcePath}`)})) return ${JSON.stringify(phase === 'before' ? baseline : source)} + } }], + 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 result = JSON.parse(await readFile(outputPath, 'utf8')) + assert.equal(result.sockets.length, 3) + assert.equal(result.controls.length, 5) + for (const row of result.sockets) { + const observed = row.scenario === 'physical-exit-observed' + const healthy = row.scenario === 'healthy' + assert.equal(row.close.ptyKilled, healthy || (observed && phase === 'after')) + assert.equal(row.fallbackKills, healthy || (observed && phase === 'after') ? 0 : 1) + assert.equal(row.targetInventoryCount, 0) + assert.equal(row.targetProbe, false) + assert.equal(row.routerProbe, healthy ? false : null) + assert.equal(row.settled.connected, false) + assert.equal(row.settled.headlessModelRetained, false) + assert.equal(row.settled.providerExitCount, 1) + assert.equal(row.settled.exitListenerCalls, 1) + if (observed) { + assert.deepEqual( + row.settled.exitCause, + phase === 'after' + ? { kind: 'operator_close' } + : { kind: 'unknown', reason: 'stop_unverified' } + ) + assert.equal(row.settled.rendererExitCount, phase === 'after' ? 1 : 2) + } + if (!healthy && !observed) { + assert.equal(row.close.ptyStopVerdict, 'unverifiable') + assert.equal(row.beforeStreamResume.providerExitCount, 0) + } + delete row.beforeStreamResume.incarnationId + delete row.settled.incarnationId + } + for (const row of result.controls) { + const accepts = phase === 'after' && row.scenario === 'same-incarnation' + assert.equal(row.stopped, accepts) + assert.equal(row.fallbackKills, accepts ? 0 : 1) + } + phases.push({ phase, ...result }) + } + const output = `${JSON.stringify( + { + sourceHashes: { + before: createHash('sha256').update(baseline).digest('hex'), + after: createHash('sha256').update(source).digest('hex'), + fixture: createHash('sha256') + .update(await readFile(join(root, fixturePath))) + .digest('hex') + }, + phases + }, + 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 }) +} diff --git a/docs/audits/terminal-close-observed-exit/results.json b/docs/audits/terminal-close-observed-exit/results.json new file mode 100644 index 00000000000..b7f79553c9a --- /dev/null +++ b/docs/audits/terminal-close-observed-exit/results.json @@ -0,0 +1,359 @@ +{ + "sourceHashes": { + "before": "963a18c7811f9bb47c5308f795edfc7ed6f7b91ccfa5c415357c701679f4204b", + "after": "36ea86fcd71c37384af31ae9b8cf8f348e4435854f5faf4829f5fafb618c2c44", + "fixture": "e9df0f19562ef2f162a6262c5052dd0e52d7770b88dc852b599e858d99452b1d" + }, + "phases": [ + { + "phase": "before", + "sockets": [ + { + "scenario": "healthy", + "close": { + "ptyKilled": true, + "ptyStopVerdict": null + }, + "fallbackKills": 0, + "targetInventoryCount": 0, + "targetProbe": false, + "routerProbe": false, + "beforeStreamResume": { + "connected": false, + "exitCause": { + "kind": "operator_close" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 0, + "exitListenerCalls": 1 + }, + "settled": { + "connected": false, + "exitCause": { + "kind": "operator_close" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 1, + "exitListenerCalls": 1 + } + }, + { + "scenario": "unrelated-endpoint-gone", + "close": { + "ptyKilled": false, + "ptyStopVerdict": "unverifiable" + }, + "fallbackKills": 1, + "targetInventoryCount": 0, + "targetProbe": false, + "routerProbe": null, + "beforeStreamResume": { + "connected": false, + "exitCause": { + "kind": "unknown", + "reason": "stop_unverified" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "unverifiable", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 0, + "exitListenerCalls": 1 + }, + "settled": { + "connected": false, + "exitCause": { + "kind": "operator_close" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 1, + "exitListenerCalls": 1 + } + }, + { + "scenario": "physical-exit-observed", + "close": { + "ptyKilled": false, + "ptyStopVerdict": null + }, + "fallbackKills": 1, + "targetInventoryCount": 0, + "targetProbe": false, + "routerProbe": null, + "beforeStreamResume": { + "connected": false, + "exitCause": { + "kind": "unknown", + "reason": "stop_unverified" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 2, + "providerExitCount": 1, + "exitListenerCalls": 1 + }, + "settled": { + "connected": false, + "exitCause": { + "kind": "unknown", + "reason": "stop_unverified" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 2, + "providerExitCount": 1, + "exitListenerCalls": 1 + } + } + ], + "controls": [ + { + "scenario": "same-incarnation", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "exited" + } + }, + { + "scenario": "replacement", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "unverifiable", + "reason": "a follow-up stop was issued but its outcome could not be verified" + } + }, + { + "scenario": "unverified", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "unverifiable", + "reason": "a follow-up stop was issued but its outcome could not be verified" + } + }, + { + "scenario": "legacy-unstamped", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "exited" + } + }, + { + "scenario": "throw-after-exit", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "unverifiable", + "reason": "unverified transport failure" + } + } + ] + }, + { + "phase": "after", + "sockets": [ + { + "scenario": "healthy", + "close": { + "ptyKilled": true, + "ptyStopVerdict": null + }, + "fallbackKills": 0, + "targetInventoryCount": 0, + "targetProbe": false, + "routerProbe": false, + "beforeStreamResume": { + "connected": false, + "exitCause": { + "kind": "operator_close" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 0, + "exitListenerCalls": 1 + }, + "settled": { + "connected": false, + "exitCause": { + "kind": "operator_close" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 1, + "exitListenerCalls": 1 + } + }, + { + "scenario": "unrelated-endpoint-gone", + "close": { + "ptyKilled": false, + "ptyStopVerdict": "unverifiable" + }, + "fallbackKills": 1, + "targetInventoryCount": 0, + "targetProbe": false, + "routerProbe": null, + "beforeStreamResume": { + "connected": false, + "exitCause": { + "kind": "unknown", + "reason": "stop_unverified" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "unverifiable", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 0, + "exitListenerCalls": 1 + }, + "settled": { + "connected": false, + "exitCause": { + "kind": "operator_close" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 1, + "exitListenerCalls": 1 + } + }, + { + "scenario": "physical-exit-observed", + "close": { + "ptyKilled": true, + "ptyStopVerdict": null + }, + "fallbackKills": 0, + "targetInventoryCount": 0, + "targetProbe": false, + "routerProbe": null, + "beforeStreamResume": { + "connected": false, + "exitCause": { + "kind": "operator_close" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 1, + "exitListenerCalls": 1 + }, + "settled": { + "connected": false, + "exitCause": { + "kind": "operator_close" + }, + "headlessModelRetained": false, + "titleTrackerRetained": false, + "liveness": "exited", + "providerHasPty": false, + "hostInventoryCount": 0, + "deliveredData": [], + "rendererExitCount": 1, + "providerExitCount": 1, + "exitListenerCalls": 1 + } + } + ], + "controls": [ + { + "scenario": "same-incarnation", + "stopped": true, + "fallbackKills": 0, + "verdict": { + "status": "exited" + } + }, + { + "scenario": "replacement", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "unverifiable", + "reason": "a follow-up stop was issued but its outcome could not be verified" + } + }, + { + "scenario": "unverified", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "unverifiable", + "reason": "a follow-up stop was issued but its outcome could not be verified" + } + }, + { + "scenario": "legacy-unstamped", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "exited" + } + }, + { + "scenario": "throw-after-exit", + "stopped": false, + "fallbackKills": 1, + "verdict": { + "status": "unverifiable", + "reason": "unverified transport failure" + } + } + ] + } + ] +} diff --git a/src/main/runtime/orca-runtime-stop-explicitly-closed-tab-ptys.ts b/src/main/runtime/orca-runtime-stop-explicitly-closed-tab-ptys.ts index 1433ecfdb0e..196845d09ef 100644 --- a/src/main/runtime/orca-runtime-stop-explicitly-closed-tab-ptys.ts +++ b/src/main/runtime/orca-runtime-stop-explicitly-closed-tab-ptys.ts @@ -15,6 +15,7 @@ export class OrcaRuntimeWithStopExplicitlyClosedTabPtys extends OrcaRuntimeWithF const deadlineMs = Date.now() + EXPLICIT_TERMINAL_CLOSE_STOP_TIMEOUT_MS for (const ptyId of ptyIds) { this.markPtyStopRequested(ptyId) + const expectedIncarnationId = this.ptysById.get(ptyId)?.incarnationId let stopped = false if (this.ptyController?.stopAndWait) { try { @@ -25,6 +26,15 @@ export class OrcaRuntimeWithStopExplicitlyClosedTabPtys extends OrcaRuntimeWithF error instanceof Error ? error.message : String(error) ) } + // Preserve an observed exit when a broader inventory check could not finish. + if ( + !stopped && + expectedIncarnationId && + this.ptysById.get(ptyId)?.incarnationId === expectedIncarnationId && + this.getPtyLivenessVerdict(ptyId)?.status === 'exited' + ) { + stopped = true + } if (!stopped) { const verdict = this.getPtyLivenessVerdict(ptyId) const providerAlreadyRetiredPty = diff --git a/src/main/runtime/terminal-close-observed-exit-test-fixture.ts b/src/main/runtime/terminal-close-observed-exit-test-fixture.ts new file mode 100644 index 00000000000..468508f896d --- /dev/null +++ b/src/main/runtime/terminal-close-observed-exit-test-fixture.ts @@ -0,0 +1,158 @@ +import { rmSync } from 'node:fs' +import { DaemonPtyRouter } from '../daemon/daemon-pty-router' +import { + createMockSubprocess, + startDaemonAdapterHarness +} from '../daemon/daemon-pty-adapter-test-harness' +import { startLateExitHarness } from '../ipc/pty/daemon-late-exit-test-fixture' +import { bindProviderListeners } from '../ipc/pty/provider/bind-listeners' +import { finishPtyShutdown } from '../ipc/pty/provider/liveness' +import { setLocalPtyProvider } from '../ipc/pty/provider/registry' +import { shutdownProviderAndDetectExit } from '../ipc/pty/provider/shutdown-detect' +import type { PtyRuntimeControllerDeps } from '../ipc/pty/runtime/controller-deps' +import { + killPtyFromRuntimeController, + stopAndWaitPtyFromRuntimeController +} from '../ipc/pty/runtime/kill' +import { OrcaRuntimeService } from './orca-runtime' + +export type ObservedExitSocketScenario = + | 'healthy' + | 'unrelated-endpoint-gone' + | 'physical-exit-observed' + +export async function runObservedExitSocketScenario(scenario: ObservedExitSocketScenario) { + const harness = await startLateExitHarness() + const legacy = await startDaemonAdapterHarness(() => createMockSubprocess()) + const router = new DaemonPtyRouter({ current: harness.adapter, legacy: [legacy.adapter] }) + let fallbackKills = 0 + try { + await router.discoverLegacySessions() + setLocalPtyProvider(router) + bindProviderListeners(harness.session) + const ports = { + runtime: harness.runtime, + getLocalPtyProviderStartupPromise: () => undefined, + shutdownProviderAndDetectExit, + rememberSyntheticKillExit: harness.session.rememberSyntheticKillExit, + sendPtyExitToRenderer: harness.session.sendPtyExitToRenderer, + finishPtyShutdown, + retiredRejectedPtyIds: new Map(), + reversibleStopOwnersByPtyId: new Map() + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: stop/kill read only these controller ports and optional store; spawn ports are unused. + const deps = ports as unknown as PtyRuntimeControllerDeps + harness.runtime.setPtyController({ + write: () => true, + getForegroundProcess: async () => null, + kill: (id) => { + fallbackKills++ + return killPtyFromRuntimeController(deps, id) + }, + stopAndWait: (id) => + stopAndWaitPtyFromRuntimeController(deps, id, { deadlineMs: Date.now() + 1_500 }) + }) + const list = await harness.runtime.listTerminals() + const terminal = list.terminals.find((entry) => entry.ptyId === harness.id) + if (!terminal) { + throw new Error('Fixture terminal missing') + } + if (scenario !== 'healthy') { + await legacy.server.shutdown() + } + if (scenario !== 'physical-exit-observed') { + harness.pauseStream() + } + const close = await harness.runtime.closeTerminal(terminal.handle) + const targetInventory = await harness.adapter.listProcesses() + const targetProbe = await harness.adapter.probePtyLiveness(harness.id) + const routerProbe = await router.probePtyLiveness(harness.id) + const beforeStreamResume = await harness.capture() + harness.resumeStream() + await harness.waitForExit() + const settled = await harness.capture() + return { + scenario, + close: { + ptyKilled: close.ptyKilled, + ptyStopVerdict: close.ptyStopVerdict ?? null + }, + fallbackKills, + targetInventoryCount: targetInventory.length, + targetProbe, + routerProbe, + beforeStreamResume, + settled + } + } finally { + router.disposeRouterOnly() + await harness.dispose() + legacy.adapter.dispose() + await legacy.server.shutdown() + rmSync(legacy.dir, { recursive: true, force: true }) + } +} + +const CONTROL_PTY_ID = 'repo::/tmp/observed-exit-control@@pty' +const WORKTREE_ID = 'repo::/tmp/observed-exit-control' +const FIRST_INCARNATION = '10000000-0000-4000-8000-000000000001' +const NEXT_INCARNATION = '10000000-0000-4000-8000-000000000002' +const BINDING = { + tabId: 'control-tab', + leafId: '10000000-0000-4000-8000-000000000004' +} + +class ObservedExitRuntime extends OrcaRuntimeService { + closeControl(): Promise { + return this.stopExplicitlyClosedTabPtys([CONTROL_PTY_ID], CONTROL_PTY_ID) + } +} + +export type ObservedExitControl = + | 'same-incarnation' + | 'replacement' + | 'unverified' + | 'legacy-unstamped' + | 'throw-after-exit' + +export async function runObservedExitControl(control: ObservedExitControl) { + const runtime = new ObservedExitRuntime() + const original = control === 'legacy-unstamped' ? undefined : FIRST_INCARNATION + let fallbackKills = 0 + runtime.registerPty(CONTROL_PTY_ID, WORKTREE_ID, null, { + ...BINDING, + ...(original ? { incarnationId: original } : {}) + }) + runtime.setPtyController({ + write: () => true, + kill: () => { + fallbackKills++ + return true + }, + getForegroundProcess: async () => null, + stopAndWait: async () => { + runtime.onPtyExit(CONTROL_PTY_ID, control === 'unverified' ? -1 : 0, original) + if (control === 'replacement') { + runtime.registerPty(CONTROL_PTY_ID, WORKTREE_ID, null, { + ...BINDING, + incarnationId: NEXT_INCARNATION + }) + } + if (control === 'throw-after-exit') { + throw new Error('unverified transport failure') + } + return false + } + }) + try { + const stopped = await runtime.closeControl() + return { + scenario: control, + stopped, + fallbackKills, + verdict: runtime.getPtyLivenessVerdict(CONTROL_PTY_ID) + } + } finally { + runtime.onPtyExit(CONTROL_PTY_ID, 0, control === 'replacement' ? NEXT_INCARNATION : original) + } +} diff --git a/src/main/runtime/terminal-close-observed-exit.test.ts b/src/main/runtime/terminal-close-observed-exit.test.ts new file mode 100644 index 00000000000..b41f61cfcc8 --- /dev/null +++ b/src/main/runtime/terminal-close-observed-exit.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest' +import { + runObservedExitControl, + runObservedExitSocketScenario +} from './terminal-close-observed-exit-test-fixture' + +describe('closing a terminal after observing its physical exit', () => { + it('preserves the physical cause when an unrelated daemon prevents aggregate verification', async () => { + const result = await runObservedExitSocketScenario('physical-exit-observed') + expect(result.close).toEqual({ ptyKilled: true, ptyStopVerdict: null }) + expect(result.fallbackKills).toBe(0) + expect(result.targetInventoryCount).toBe(0) + expect(result.targetProbe).toBe(false) + expect(result.routerProbe).toBeNull() + expect(result.settled).toMatchObject({ + connected: false, + exitCause: { kind: 'operator_close' }, + headlessModelRetained: false, + titleTrackerRetained: false, + liveness: 'exited', + rendererExitCount: 1, + providerExitCount: 1, + exitListenerCalls: 1 + }) + }) + + it('keeps the healthy aggregate verification and delayed physical exit behavior', async () => { + const result = await runObservedExitSocketScenario('healthy') + expect(result.close.ptyKilled).toBe(true) + expect(result.fallbackKills).toBe(0) + expect(result.routerProbe).toBe(false) + expect(result.settled.exitCause).toEqual({ kind: 'operator_close' }) + expect(result.settled.rendererExitCount).toBe(1) + expect(result.settled.exitListenerCalls).toBe(1) + }) + + it('does not treat target absence as an earned exit before the stream delivers it', async () => { + const result = await runObservedExitSocketScenario('unrelated-endpoint-gone') + expect(result.close).toEqual({ ptyKilled: false, ptyStopVerdict: 'unverifiable' }) + expect(result.fallbackKills).toBe(1) + expect(result.targetProbe).toBe(false) + expect(result.routerProbe).toBeNull() + expect(result.beforeStreamResume.providerExitCount).toBe(0) + }) + + it('uses a stamped exit for the incarnation that was actually being closed', async () => { + const result = await runObservedExitControl('same-incarnation') + expect(result.stopped).toBe(true) + expect(result.fallbackKills).toBe(0) + expect(result.verdict?.status).toBe('exited') + }) + + it.each(['replacement', 'unverified', 'legacy-unstamped', 'throw-after-exit'] as const)( + 'does not reuse an exit for %s', + async (control) => { + const result = await runObservedExitControl(control) + expect(result.stopped).toBe(false) + expect(result.fallbackKills).toBe(1) + } + ) +}) From 57e28ccf7c523b57881be0b857fcc8e171a17b05 Mon Sep 17 00:00:00 2001 From: Lesley Murfin Date: Thu, 17 Sep 2026 21:33:36 -0600 Subject: [PATCH 34/59] fix(runtime): keep absent session tab close intents durable (#21189) (#21277) * fix(runtime): treat selector_not_found as definitive tab absence (#21189) When closing a tab whose worktree selector is absent, propagate the error through host RPC and classify it as unknown-tab on the renderer to engage durable tombstones and prevent resurrection loops. Pin host RPC error propagation with dedicated regression tests. Co-authored-by: Neil Parker * test(runtime): remove invalid absent-tab Docker spec The spec dynamically imported renderer source from the browser and did not exercise a real close RPC. Keep the executable renderer and host regression coverage instead.\n\nCo-authored-by: Lesley Murfin * fix(runtime): narrow durable tab absence to tab and terminal absence (#21189) Narrow durable close tombstones in web-runtime-session-tab-lifecycle to tab_not_found and terminal_tab_not_found. In production, session tab close requests pass explicit `id:` worktree selectors and take the fast path in closeMobileSessionTab, bypassing resolveWorktreeSelector. Transient selector_not_found errors retain normal TTL eviction. --------- Co-authored-by: Neil Parker --- .../web-runtime-session-tab-activate-close.test.ts | 7 +++++++ .../runtime/web-runtime-session-tab-lifecycle.ts | 13 +++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/runtime/web-runtime-session-tab-activate-close.test.ts b/src/renderer/src/runtime/web-runtime-session-tab-activate-close.test.ts index b535bd31836..495640cde79 100644 --- a/src/renderer/src/runtime/web-runtime-session-tab-activate-close.test.ts +++ b/src/renderer/src/runtime/web-runtime-session-tab-activate-close.test.ts @@ -288,8 +288,12 @@ describe('web runtime session tab actions', () => { // Why this distinction is load-bearing: a close that reports 'unknown-tab' lets the client // finish a teardown the host cannot, and reporting it for an ordinary failure would tear down // tabs a reachable host still holds. + // Note: 'selector_not_found' is a transient scan cache miss during worktree discovery, not + // definitive absence proof, so it classifies as 'failed' and must not drop TTL eviction. it.each([ ['tab_not_found', 'unknown-tab'], + ['selector_not_found', 'failed'], + ['terminal_tab_not_found', 'unknown-tab'], ['runtime_rpc_timeout', 'failed'] ])('classifies a %s close refusal as %s', async (code, outcome) => { const runtimeCall = vi @@ -310,8 +314,11 @@ describe('web runtime session tab actions', () => { // #9194: a host can answer tab_not_found and still keep republishing the surface. The close // intent is what hides the mirror, so letting it age out handed the user back a phantom pane // whose handle is already gone -- and closing it again just restarted the same TTL loop. + // 'selector_not_found' is transient, so it does not become durable and its suppression expires. it.each([ ['tab_not_found', true], + ['selector_not_found', false], + ['terminal_tab_not_found', true], ['runtime_rpc_timeout', false] ])('keeps a %s close suppressed past the close-intent TTL: %s', async (code, stillPending) => { const runtimeCall = vi diff --git a/src/renderer/src/runtime/web-runtime-session-tab-lifecycle.ts b/src/renderer/src/runtime/web-runtime-session-tab-lifecycle.ts index 56800f3cefb..0fbb18fb534 100644 --- a/src/renderer/src/runtime/web-runtime-session-tab-lifecycle.ts +++ b/src/renderer/src/runtime/web-runtime-session-tab-lifecycle.ts @@ -162,10 +162,15 @@ async function callWebRuntimeSessionTabMethod( if (activationHostTabId) { clearWebSessionFocusIntentIfMatches(intentOwner, args.worktreeId, activationHostTabId) } - // Why the split: only 'tab_not_found' is absence proof (see the outcome doc above). Restoring the - // mirror on it hands the user back a pane the host cannot close and whose handle is already gone - // (#9194), so keep the suppression and drop its TTL instead. Every other failure is a "not now". - const hostHasNoSuchTab = hasRuntimeRpcErrorCode(error, 'tab_not_found') + // Why the split: 'tab_not_found' and 'terminal_tab_not_found' prove definitive surface absence. + // Restoring the mirror on it hands the user back a pane the host cannot close and whose handle is already gone (#9194, #21189), + // so keep the suppression and drop its TTL instead. + // 'selector_not_found' is a transient worktree resolver state (e.g. during scans or cache warm-up, + // per remote-browser-stream-errors.ts) and must not become a durable close tombstone. + // Every other failure is a "not now". + const hostHasNoSuchTab = + hasRuntimeRpcErrorCode(error, 'tab_not_found') || + hasRuntimeRpcErrorCode(error, 'terminal_tab_not_found') for (const hostTabId of closeIntentTabIds) { if (hostHasNoSuchTab) { makeWebSessionCloseIntentDurable(intentOwner, args.worktreeId, hostTabId) From d04b05b5c8f0b8074a7e316c8cd6ccdbbddf7e2c Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:34:19 -0700 Subject: [PATCH 35/59] Detach retained CI and terminal tails from oversized strings (#20960) * fix(memory): detach retained CI and terminal tails from oversized strings * fix(terminal): detach retained error and reattach string slices * fix(terminal): release oversized recent-output backing strings * fix(terminal): release backing strings held by PTY detectors * fix(memory): own bounded Claude background task labels * fix: detach retained terminal mode scan tails * fix: own retained plugin worker output strings * fix: own incomplete OSC 133 carry strings --------- Co-authored-by: m4air Co-authored-by: m4air --- docs/audits/claude-task-retention/README.md | 64 + .../claude-task-retention/reproduce.cjs | 197 +++ .../audits/claude-task-retention/results.json | 370 ++++ docs/audits/osc133-carry-retention/README.md | 106 ++ .../osc133-carry-retention/before.config.mjs | 23 + .../electron-results.json | 1535 +++++++++++++++++ docs/audits/osc133-carry-retention/fix.patch | 7 + .../osc133-carry-retention/node-results.json | 1535 +++++++++++++++++ .../osc133-carry-retention/reproduce.cjs | 142 ++ .../osc133-carry-retention/scenario.cjs | 99 ++ .../source-versions.json | 646 +++++++ .../audits/osc133-carry-retention/sources.cjs | 87 + .../osc133-carry-retention/validation.json | 83 + .../plugin-worker-output-retention/README.md | 67 + .../before.config.mjs | 23 + .../electron-results.json | 493 ++++++ .../plugin-worker-output-retention/fix.patch | 18 + .../node-results.json | 492 ++++++ .../reproduce.cjs | 249 +++ .../source-versions.json | 89 + .../sources.cjs | 94 + docs/audits/pty-detector-retention/README.md | 59 + .../pty-detector-retention/reproduce.mjs | 144 ++ .../pty-detector-retention/results.json | 94 + docs/audits/retained-text-slices/README.md | 75 + .../audits/retained-text-slices/reproduce.mjs | 161 ++ docs/audits/retained-text-slices/results.json | 140 ++ .../terminal-mode-tail-retention/README.md | 122 ++ .../electron-results.json | 467 +++++ .../load-source.cjs | 68 + .../node-results.json | 467 +++++ .../reproduce.cjs | 205 +++ .../source-versions.json | 44 + .../claude/claude-background-task-frames.ts | 3 +- .../claude-background-task-retention.test.ts | 96 ++ src/main/daemon/terminal-mouse-mode-mirror.ts | 5 +- .../terminal-mouse-tail-retention.test.ts | 41 + .../plugins/plugin-worker-output-buffer.ts | 11 +- .../plugin-worker-output-retention.test.ts | 82 + src/main/ports/advertised-url-parsing.ts | 3 +- .../ports/advertised-url-retention.test.ts | 44 + src/main/ports/advertised-url-watcher.ts | 7 +- src/main/runtime/recent-pty-output-buffer.ts | 4 +- .../recent-pty-output-retention.test.ts | 35 + .../deferred-reattach-live-data-queue.ts | 5 +- .../terminal-pane/pty-eager-buffer-clamp.ts | 6 +- .../terminal-capped-buffer-retention.test.ts | 92 + .../terminal-error-accumulation.ts | 6 +- src/shared/check-job-log-retention.test.ts | 45 + src/shared/check-job-log-tail-slice.ts | 8 +- .../command-code-output-retention.test.ts | 26 + src/shared/command-code-output-status.ts | 3 +- .../terminal-kitty-keyboard-mode-tracker.ts | 3 +- ...inal-kitty-keyboard-tail-retention.test.ts | 41 + .../terminal-osc133-carry-retention.test.ts | 109 ++ .../terminal-osc133-command-finished.ts | 3 + .../workspace-session-terminal-buffers.ts | 5 +- 57 files changed, 9130 insertions(+), 18 deletions(-) create mode 100644 docs/audits/claude-task-retention/README.md create mode 100644 docs/audits/claude-task-retention/reproduce.cjs create mode 100644 docs/audits/claude-task-retention/results.json create mode 100644 docs/audits/osc133-carry-retention/README.md create mode 100644 docs/audits/osc133-carry-retention/before.config.mjs create mode 100644 docs/audits/osc133-carry-retention/electron-results.json create mode 100644 docs/audits/osc133-carry-retention/fix.patch create mode 100644 docs/audits/osc133-carry-retention/node-results.json create mode 100644 docs/audits/osc133-carry-retention/reproduce.cjs create mode 100644 docs/audits/osc133-carry-retention/scenario.cjs create mode 100644 docs/audits/osc133-carry-retention/source-versions.json create mode 100644 docs/audits/osc133-carry-retention/sources.cjs create mode 100644 docs/audits/osc133-carry-retention/validation.json create mode 100644 docs/audits/plugin-worker-output-retention/README.md create mode 100644 docs/audits/plugin-worker-output-retention/before.config.mjs create mode 100644 docs/audits/plugin-worker-output-retention/electron-results.json create mode 100644 docs/audits/plugin-worker-output-retention/fix.patch create mode 100644 docs/audits/plugin-worker-output-retention/node-results.json create mode 100644 docs/audits/plugin-worker-output-retention/reproduce.cjs create mode 100644 docs/audits/plugin-worker-output-retention/source-versions.json create mode 100644 docs/audits/plugin-worker-output-retention/sources.cjs create mode 100644 docs/audits/pty-detector-retention/README.md create mode 100644 docs/audits/pty-detector-retention/reproduce.mjs create mode 100644 docs/audits/pty-detector-retention/results.json create mode 100644 docs/audits/retained-text-slices/README.md create mode 100644 docs/audits/retained-text-slices/reproduce.mjs create mode 100644 docs/audits/retained-text-slices/results.json create mode 100644 docs/audits/terminal-mode-tail-retention/README.md create mode 100644 docs/audits/terminal-mode-tail-retention/electron-results.json create mode 100644 docs/audits/terminal-mode-tail-retention/load-source.cjs create mode 100644 docs/audits/terminal-mode-tail-retention/node-results.json create mode 100644 docs/audits/terminal-mode-tail-retention/reproduce.cjs create mode 100644 docs/audits/terminal-mode-tail-retention/source-versions.json create mode 100644 src/main/claude/claude-background-task-retention.test.ts create mode 100644 src/main/daemon/terminal-mouse-tail-retention.test.ts create mode 100644 src/main/plugins/plugin-worker-output-retention.test.ts create mode 100644 src/main/ports/advertised-url-retention.test.ts create mode 100644 src/main/runtime/recent-pty-output-retention.test.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-capped-buffer-retention.test.ts create mode 100644 src/shared/check-job-log-retention.test.ts create mode 100644 src/shared/command-code-output-retention.test.ts create mode 100644 src/shared/terminal-kitty-keyboard-tail-retention.test.ts create mode 100644 src/shared/terminal-osc133-carry-retention.test.ts diff --git a/docs/audits/claude-task-retention/README.md b/docs/audits/claude-task-retention/README.md new file mode 100644 index 00000000000..da40c172667 --- /dev/null +++ b/docs/audits/claude-task-retention/README.md @@ -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. diff --git a/docs/audits/claude-task-retention/reproduce.cjs b/docs/audits/claude-task-retention/reproduce.cjs new file mode 100644 index 00000000000..5910c10f847 --- /dev/null +++ b/docs/audits/claude-task-retention/reproduce.cjs @@ -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 +}) diff --git a/docs/audits/claude-task-retention/results.json b/docs/audits/claude-task-retention/results.json new file mode 100644 index 00000000000..8d337835310 --- /dev/null +++ b/docs/audits/claude-task-retention/results.json @@ -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 + } + ] +} diff --git a/docs/audits/osc133-carry-retention/README.md b/docs/audits/osc133-carry-retention/README.md new file mode 100644 index 00000000000..e49a2aea734 --- /dev/null +++ b/docs/audits/osc133-carry-retention/README.md @@ -0,0 +1,106 @@ +# Retained OSC 133 incomplete carry + +The shared command-lifecycle scanner keeps an incomplete OSC 133 suffix of at +most 4,096 UTF-16 code units. A V8 sliced string can keep the entire preceding +PTY input alive through that small suffix. The correction copies only the final +incomplete carry through existing `ownRetainedString`; short prefixes, content, +parsing, callbacks, authority and reset behavior are preserved. + +This adds the fifteenth retained-text boundary to +[#20960](https://github.com/stablyai/orca/pull/20960), following the +[kitty/mouse tails](../terminal-mode-tail-retention/README.md) and other +[retained text slices](../retained-text-slices/README.md). It introduces no wire +change and applies equally to local and SSH/remote terminal bytes reaching the +shared scanner. + +## Reproduce + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/osc133-carry-retention/reproduce.cjs +``` + +Run the same script with the installed Electron executable, setting +`ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`, with the same Node flags. +No application window, native PTY, or network is created. The runner has a +60-second deadline and accepts an optional output-report path as its first +argument; otherwise it writes [Node](./node-results.json) or +[Electron](./electron-results.json) results here. + +The portable loader validates all 28 bundled source modules and seven additional +caller/fixture files. Non-evaluated provenance callers accept only the recorded +audited or named-main bytes, and reports identify which was present; evaluated +modules each require one exact fixed hash. It reverses only the new import/copy call in memory through +a zero-context [patch](./fix.patch), then validates the baseline hash. All +evaluated-source and artifact hashes are recorded. It needs no Git history, +ignored notes, or absolute developer paths. Source/patch reads normalize CRLF; +an in-memory CRLF control checks equivalent before/after strings. + +The scanner baseline exactly matches named main +`291b4ddd6f1c1af480169885e0fda7f9c78ff053` and `v1.4.198` +(`e0826956fcfc532f5a1e55b5e081f2e57e553c43`). The copier did not exist in +`v1.4.198`; current helpers and callers are used for both sides of this +experiment. [Source provenance](./source-versions.json) records each named +identity/absence separately. This is not a replay of a complete historical app. + +## Result and controls + +Both Node 26.6 and Electron 43.7 / Node 24.21 pass **117 cases**. Each runtime +compares the baseline, fixed Buffer copier and fixed Bufferless copier through +the actual scanner, shared title tracker, and daemon background transient-fact +relay. Thirty-two 64 Ki-character inputs retain roughly 2 MiB before the copy; +eight 1 Mi-character inputs retain roughly 8 MiB. Fixed deltas are below the +asserted 1 MiB tolerance, including owner overhead. Completion and reset/exit +release the old parents. Exact GC-sensitive measurements are in the reports; +they are heap deltas, not RSS or exact allocation attribution. + +The ordinary sequence bytes come from the fish 4.7.1 capture documented in +`src/shared/terminal-mode-2031-final-state.test.ts`: `A;click_events=1` and +`C;cmdline_url=npx`. That capture contains complete OSC sequences. **The large +plain-output prefix and cut before the terminator are synthetic.** This does +not claim the original capture had those sizes or boundaries. + +Controls preserve BEL/ST completion, split prefixes, C/D callback values, +background disable/re-enable, reset, and every split of a Unicode/NUL/lone- +surrogate fixture. The Bufferless copier is selected while Buffer is absent, +then Buffer is restored before measurement; this exercises the renderer's +actual fallback without launching a renderer. Short ordinary `D;0` prefixes, +complete sequences and plain input are negative retention controls. Oversized +unterminated input is separately labelled malformed-protocol stress. V8's +independent last successful RegExp input is reset before both measurements. + +Eight permanent tests cover both copier paths, long captured-fish suffixes, +completion, reset and short `D;0`. With the in-memory baseline overlay, exactly +four long-suffix regressions fail at 33,550,680–33,565,360 retained bytes against +a 2 MiB allowance; the other 41 tests in the four-suite run pass. The fixed run +passes all 45. Wider proof/quality validation is recorded in +[validation.json](./validation.json). + +## Owners and ordinary input bounds + +Main creates a per-PTY tracker with `onCommandFinished` in +`orca-runtime-get-unpersisted-tracked-title-for-pty.ts`; scanner enablement still +respects transient-fact consumer/authority state. Ordinary daemon output frames +delivered to main are sliced to 64 Ki characters in +`daemon-stream-data-batcher.ts`, and ordinary relay output to 16 Ki characters +in `src/relay/pty-handler.ts`. The 64 Ki cases therefore demonstrate retention +without requiring a multi-megabyte main input; the 1 Mi cases amplify the +mechanism. Replay and transformed output have their own existing limits. + +The daemon's `BackgroundTransientFactRelay` owns one tracker per background +session. `daemon-terminal-admission.ts` feeds it before output batching, so the +batcher's later slicing is not an input cap on this daemon scanner. Native data +passes through `pty-subprocess/subprocess-handle.ts`, the session's shell +readiness/startup/recovery path, and its stream client. The inspected local +intake does not impose an independent string-length limit; platform/native +library chunk sizes were not measured here. + +Completion/replacement of the incomplete escape, scanner reset, session exit, +background retirement, tracker disposal or owner release drops the old parent. +This is at most the last incomplete-parent cost per live scanner, not a list of +every historical chunk. Multiple readers may share the same input backing +storage; do not add their isolated measurements as independent process totals. + +This is a reproduced code-level retention mechanism. It does not establish a +native output pause, normal-session frequency/duration, the trigger in +#19831/#19768, a reported sustained growth rate, or a multi-gigabyte incident's +cause. The change does not reduce original input allocation. diff --git a/docs/audits/osc133-carry-retention/before.config.mjs b/docs/audits/osc133-carry-retention/before.config.mjs new file mode 100644 index 00000000000..c1ca6c982ba --- /dev/null +++ b/docs/audits/osc133-carry-retention/before.config.mjs @@ -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, versions } = createRequire(import.meta.url)('./sources.cjs') +const { baseline } = loadSources() +const target = resolve(versions.sourcePath) + +export default mergeConfig( + baseConfig, + defineConfig({ + plugins: [ + { + name: 'osc133-before-owned-carry', + enforce: 'pre', + transform(_source, id) { + return resolve(id.split('?')[0]) === target ? { code: baseline, map: null } : undefined + } + } + ] + }) +) diff --git a/docs/audits/osc133-carry-retention/electron-results.json b/docs/audits/osc133-carry-retention/electron-results.json new file mode 100644 index 00000000000..f515770b5ec --- /dev/null +++ b/docs/audits/osc133-carry-retention/electron-results.json @@ -0,0 +1,1535 @@ +{ + "scope": "Actual-source bounded diagnostic; captured fish sequence syntax with synthetic large-prefix placement and chunk split. No affected-host, RSS, native PTY, or whole-release claim.", + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "sourcePath": "src/shared/terminal-osc133-command-finished.ts", + "crlfReads": 2, + "artifactHashes": { + "sources.cjs": "90404793218081e62aabd7649ce83fb1ec026e3c3bf95f056a190c7ef1cd0c08", + "scenario.cjs": "f3ebcfe65d68d05e392803161acac38a5958a69d7f84e3943a0cc293dd73121e", + "reproduce.cjs": "b84497f3bc4b9541347b84fa3d2fac57289519a329287d67a44e5fad6d35ac3d", + "source-versions.json": "3ad70d2c35b97b01ff729146240882d3a8b1c5d06989236e88b3574b5aec2f70", + "fix.patch": "cd48c5d6fd32fc5e7fcdc682a9b92c875ba5f08d975e8330f495e83d1377bf64", + "before.config.mjs": "593fb80a8506cfc226b0663964fb7690ceccf9d641c91230d923823bde291788" + }, + "versions": [ + { + "variant": "baseline", + "fixed": false, + "sourceSha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd", + "bundleSha256": "994f6a288ddd6e16990410baa0304d4b1b240ff544978a8441321cd238f09306", + "evaluatedSources": { + "src/shared/terminal-output-side-effects.ts": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49", + "src/main/daemon/daemon-background-transient-facts.ts": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "src/shared/terminal-osc133-command-finished.ts": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-github-pr-link-detector.ts": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-bell-detector.ts": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "src/shared/terminal-color-scheme-protocol.ts": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/github/links.ts": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + "callerSourceHashes": { + "src/main/daemon/daemon-stream-data-batcher.ts": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe", + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/pty-subprocess/subprocess-handle.ts": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "src/main/daemon/session.ts": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338", + "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea", + "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "src/shared/terminal-mode-2031-final-state.test.ts": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10" + }, + "controls": { + "emitted": [["started"], ["finished", 137], ["finished", 0], ["finished", null]], + "facts": [ + [ + "s", + { + "kind": "command-finished", + "exitCode": 137 + } + ], + [ + "s", + { + "kind": "bell" + } + ] + ], + "splitResults": [ + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890] + ] + } + }, + { + "variant": "candidate-buffer", + "fixed": true, + "sourceSha256": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "bundleSha256": "43064795284df44470398f46a53dc21489dcabe9db0bbb1c3f087a1580767b75", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/daemon/daemon-background-transient-facts.ts": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "src/shared/terminal-output-side-effects.ts": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49", + "src/shared/terminal-osc133-command-finished.ts": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-color-scheme-protocol.ts": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-github-pr-link-detector.ts": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "src/shared/terminal-bell-detector.ts": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/github/links.ts": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + "callerSourceHashes": { + "src/main/daemon/daemon-stream-data-batcher.ts": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe", + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/pty-subprocess/subprocess-handle.ts": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "src/main/daemon/session.ts": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338", + "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea", + "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "src/shared/terminal-mode-2031-final-state.test.ts": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10" + }, + "controls": { + "emitted": [["started"], ["finished", 137], ["finished", 0], ["finished", null]], + "facts": [ + [ + "s", + { + "kind": "command-finished", + "exitCode": 137 + } + ], + [ + "s", + { + "kind": "bell" + } + ] + ], + "splitResults": [ + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890] + ] + } + }, + { + "variant": "candidate-fallback", + "fixed": true, + "sourceSha256": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "bundleSha256": "43064795284df44470398f46a53dc21489dcabe9db0bbb1c3f087a1580767b75", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-osc133-command-finished.ts": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "src/main/daemon/daemon-background-transient-facts.ts": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "src/shared/terminal-output-side-effects.ts": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-color-scheme-protocol.ts": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "src/shared/terminal-github-pr-link-detector.ts": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-bell-detector.ts": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/github/links.ts": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + "callerSourceHashes": { + "src/main/daemon/daemon-stream-data-batcher.ts": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe", + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/pty-subprocess/subprocess-handle.ts": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "src/main/daemon/session.ts": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338", + "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea", + "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "src/shared/terminal-mode-2031-final-state.test.ts": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10" + }, + "controls": { + "emitted": [["started"], ["finished", 137], ["finished", 0], ["finished", null]], + "facts": [ + [ + "s", + { + "kind": "command-finished", + "exitCode": 137 + } + ], + [ + "s", + { + "kind": "bell" + } + ] + ], + "splitResults": [ + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890] + ] + } + } + ], + "reports": [ + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 2119572, + "completedDelta": 22604 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 8382328, + "completedDelta": 26640 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 2105272, + "completedDelta": 7248 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 8382472, + "completedDelta": -5888 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 7876, + "completedDelta": 7252 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -6344, + "completedDelta": -6536 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 8032, + "completedDelta": 8064 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -6536, + "completedDelta": -6536 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 7160, + "completedDelta": 7160 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -6536, + "completedDelta": -6536 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 2105312, + "completedDelta": 7136 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 8382328, + "completedDelta": -6536 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "reset-without-completion", + "resetDelta": 2512 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 2127668, + "completedDelta": 29824 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 8372484, + "completedDelta": -16380 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 2115972, + "completedDelta": 17796 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 8372484, + "completedDelta": -16380 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 19708, + "completedDelta": 19060 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -16188, + "completedDelta": -16380 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 17796, + "completedDelta": 17796 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -16380, + "completedDelta": -16380 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 20928, + "completedDelta": 20928 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -16380, + "completedDelta": -16380 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 2125312, + "completedDelta": 27136 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 8374304, + "completedDelta": -14560 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "reset-without-completion", + "resetDelta": 3512 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 2167588, + "completedDelta": 69168 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 8394340, + "completedDelta": 5316 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 2151636, + "completedDelta": 52820 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 8395036, + "completedDelta": 6012 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 46472, + "completedDelta": 48232 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": 5608, + "completedDelta": 5256 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 45640, + "completedDelta": 49600 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": 5416, + "completedDelta": 5256 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 39900, + "completedDelta": 45920 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": 84, + "completedDelta": 84 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 2144416, + "completedDelta": 45600 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 8394880, + "completedDelta": 5856 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "reset-without-completion", + "resetDelta": -13700 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 7620, + "completedDelta": 8732 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": -6248, + "completedDelta": -6536 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 8248, + "completedDelta": 7096 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": -6248, + "completedDelta": -6536 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 7864, + "completedDelta": 8100 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -6344, + "completedDelta": -6536 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 7096, + "completedDelta": 7128 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -6536, + "completedDelta": -6536 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 7160, + "completedDelta": 7160 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -6536, + "completedDelta": -6536 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 138568, + "completedDelta": 7112 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 26328, + "completedDelta": -6536 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "reset-without-completion", + "resetDelta": -592 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 34752, + "completedDelta": 33664 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": -16164, + "completedDelta": -16452 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 18948, + "completedDelta": 17796 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": -16092, + "completedDelta": -16380 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 18564, + "completedDelta": 17796 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -16188, + "completedDelta": -16380 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 17796, + "completedDelta": 17796 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -16380, + "completedDelta": -16380 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 20884, + "completedDelta": 20884 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -16380, + "completedDelta": -16380 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 168044, + "completedDelta": 36588 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 18248, + "completedDelta": -14616 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "reset-without-completion", + "resetDelta": 3424 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 61964, + "completedDelta": 60356 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 5704, + "completedDelta": 5256 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 54568, + "completedDelta": 52776 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 5704, + "completedDelta": 5256 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 46472, + "completedDelta": 45168 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": 5608, + "completedDelta": 5256 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 45640, + "completedDelta": 49604 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": 5416, + "completedDelta": 5256 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 39900, + "completedDelta": 39900 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": 140, + "completedDelta": 140 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 177696, + "completedDelta": 45600 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 38880, + "completedDelta": 5856 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "reset-without-completion", + "resetDelta": -14268 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 9712, + "completedDelta": 9332 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": -6248, + "completedDelta": -6536 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 8248, + "completedDelta": 7096 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": -6248, + "completedDelta": -6536 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 7864, + "completedDelta": 7096 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -6344, + "completedDelta": -6536 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 7096, + "completedDelta": 7096 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -6536, + "completedDelta": -6536 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 7128, + "completedDelta": 7128 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -6536, + "completedDelta": -6536 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 147476, + "completedDelta": 16020 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 26328, + "completedDelta": -6536 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "reset-without-completion", + "resetDelta": -592 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 22252, + "completedDelta": 22168 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": -16092, + "completedDelta": -16380 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 18948, + "completedDelta": 17796 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": -16092, + "completedDelta": -16380 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 18564, + "completedDelta": 17796 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -16188, + "completedDelta": -16380 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 17796, + "completedDelta": 17796 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -16380, + "completedDelta": -16380 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 20844, + "completedDelta": 20844 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -10132, + "completedDelta": -10132 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 164608, + "completedDelta": 27656 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 16484, + "completedDelta": -16380 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "reset-without-completion", + "resetDelta": 1884 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 53476, + "completedDelta": 51684 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 5704, + "completedDelta": 7968 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 46876, + "completedDelta": 45084 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 5704, + "completedDelta": 5256 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 46440, + "completedDelta": 45080 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": 5608, + "completedDelta": 5256 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 50212, + "completedDelta": 49616 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": 5416, + "completedDelta": 5256 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 39900, + "completedDelta": 39900 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": 140, + "completedDelta": 140 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 177696, + "completedDelta": 45600 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 38880, + "completedDelta": 5856 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "reset-without-completion", + "resetDelta": -25044 + } + ] +} diff --git a/docs/audits/osc133-carry-retention/fix.patch b/docs/audits/osc133-carry-retention/fix.patch new file mode 100644 index 00000000000..06455a072aa --- /dev/null +++ b/docs/audits/osc133-carry-retention/fix.patch @@ -0,0 +1,7 @@ +--- a/src/shared/terminal-osc133-command-finished.ts ++++ b/src/shared/terminal-osc133-command-finished.ts +@@ -9,0 +10,2 @@ ++ ++import { ownRetainedString } from './own-retained-string' +@@ -93,0 +96 @@ ++ carry = ownRetainedString(carry) diff --git a/docs/audits/osc133-carry-retention/node-results.json b/docs/audits/osc133-carry-retention/node-results.json new file mode 100644 index 00000000000..a6ffeda453e --- /dev/null +++ b/docs/audits/osc133-carry-retention/node-results.json @@ -0,0 +1,1535 @@ +{ + "scope": "Actual-source bounded diagnostic; captured fish sequence syntax with synthetic large-prefix placement and chunk split. No affected-host, RSS, native PTY, or whole-release claim.", + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "sourcePath": "src/shared/terminal-osc133-command-finished.ts", + "crlfReads": 2, + "artifactHashes": { + "sources.cjs": "90404793218081e62aabd7649ce83fb1ec026e3c3bf95f056a190c7ef1cd0c08", + "scenario.cjs": "f3ebcfe65d68d05e392803161acac38a5958a69d7f84e3943a0cc293dd73121e", + "reproduce.cjs": "b84497f3bc4b9541347b84fa3d2fac57289519a329287d67a44e5fad6d35ac3d", + "source-versions.json": "3ad70d2c35b97b01ff729146240882d3a8b1c5d06989236e88b3574b5aec2f70", + "fix.patch": "cd48c5d6fd32fc5e7fcdc682a9b92c875ba5f08d975e8330f495e83d1377bf64", + "before.config.mjs": "593fb80a8506cfc226b0663964fb7690ceccf9d641c91230d923823bde291788" + }, + "versions": [ + { + "variant": "baseline", + "fixed": false, + "sourceSha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd", + "bundleSha256": "994f6a288ddd6e16990410baa0304d4b1b240ff544978a8441321cd238f09306", + "evaluatedSources": { + "src/main/daemon/daemon-background-transient-facts.ts": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "src/shared/terminal-osc133-command-finished.ts": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-output-side-effects.ts": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49", + "src/shared/terminal-color-scheme-protocol.ts": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-github-pr-link-detector.ts": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-bell-detector.ts": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/github/links.ts": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + "callerSourceHashes": { + "src/main/daemon/daemon-stream-data-batcher.ts": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe", + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/pty-subprocess/subprocess-handle.ts": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "src/main/daemon/session.ts": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338", + "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea", + "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "src/shared/terminal-mode-2031-final-state.test.ts": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10" + }, + "controls": { + "emitted": [["started"], ["finished", 137], ["finished", 0], ["finished", null]], + "facts": [ + [ + "s", + { + "kind": "command-finished", + "exitCode": 137 + } + ], + [ + "s", + { + "kind": "bell" + } + ] + ], + "splitResults": [ + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890] + ] + } + }, + { + "variant": "candidate-buffer", + "fixed": true, + "sourceSha256": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "bundleSha256": "43064795284df44470398f46a53dc21489dcabe9db0bbb1c3f087a1580767b75", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-osc133-command-finished.ts": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "src/main/daemon/daemon-background-transient-facts.ts": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "src/shared/terminal-output-side-effects.ts": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49", + "src/shared/terminal-github-pr-link-detector.ts": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-bell-detector.ts": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "src/shared/terminal-color-scheme-protocol.ts": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/github/links.ts": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + "callerSourceHashes": { + "src/main/daemon/daemon-stream-data-batcher.ts": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe", + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/pty-subprocess/subprocess-handle.ts": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "src/main/daemon/session.ts": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338", + "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea", + "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "src/shared/terminal-mode-2031-final-state.test.ts": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10" + }, + "controls": { + "emitted": [["started"], ["finished", 137], ["finished", 0], ["finished", null]], + "facts": [ + [ + "s", + { + "kind": "command-finished", + "exitCode": 137 + } + ], + [ + "s", + { + "kind": "bell" + } + ] + ], + "splitResults": [ + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890] + ] + } + }, + { + "variant": "candidate-fallback", + "fixed": true, + "sourceSha256": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "bundleSha256": "43064795284df44470398f46a53dc21489dcabe9db0bbb1c3f087a1580767b75", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-output-side-effects.ts": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49", + "src/shared/terminal-osc133-command-finished.ts": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "src/main/daemon/daemon-background-transient-facts.ts": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-github-pr-link-detector.ts": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-bell-detector.ts": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "src/shared/terminal-color-scheme-protocol.ts": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/github/links.ts": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + "callerSourceHashes": { + "src/main/daemon/daemon-stream-data-batcher.ts": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe", + "src/main/daemon/daemon-terminal-admission.ts": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "src/main/daemon/pty-subprocess/subprocess-handle.ts": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "src/main/daemon/session.ts": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338", + "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea", + "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "src/shared/terminal-mode-2031-final-state.test.ts": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10" + }, + "controls": { + "emitted": [["started"], ["finished", 137], ["finished", 0], ["finished", null]], + "facts": [ + [ + "s", + { + "kind": "command-finished", + "exitCode": 137 + } + ], + [ + "s", + { + "kind": "bell" + } + ] + ], + "splitResults": [ + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890], + [1234567890] + ] + } + } + ], + "reports": [ + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 2129640, + "completedDelta": 40400 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 8376888, + "completedDelta": -12104 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 2112880, + "completedDelta": 14192 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 8375904, + "completedDelta": -12464 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 15232, + "completedDelta": 14480 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -12816, + "completedDelta": -13072 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 14192, + "completedDelta": 14480 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -13072, + "completedDelta": -13072 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 14320, + "completedDelta": 14384 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -12800, + "completedDelta": -11920 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 2112960, + "completedDelta": 14272 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 8375920, + "completedDelta": -13072 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "scanner", + "input": "reset-without-completion", + "resetDelta": 4984 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 2149040, + "completedDelta": 50976 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 8356232, + "completedDelta": -32760 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 2134280, + "completedDelta": 35592 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 8356232, + "completedDelta": -32760 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 38088, + "completedDelta": 37288 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -32504, + "completedDelta": -32760 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 35592, + "completedDelta": 35592 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -32760, + "completedDelta": -32760 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 38800, + "completedDelta": 38800 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -32760, + "completedDelta": -32760 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 2144864, + "completedDelta": 46176 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 8358200, + "completedDelta": -30792 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "title-tracker", + "input": "reset-without-completion", + "resetDelta": 7008 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 2203896, + "completedDelta": 113936 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 8395000, + "completedDelta": 5688 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 2189328, + "completedDelta": 91960 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 8394872, + "completedDelta": 6464 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 91008, + "completedDelta": 89120 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": 6064, + "completedDelta": 5488 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 86064, + "completedDelta": 89528 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": 5808, + "completedDelta": 5488 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 79544, + "completedDelta": 80520 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": 216, + "completedDelta": 216 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 2185568, + "completedDelta": 85600 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 8395616, + "completedDelta": 6304 + }, + { + "variant": "baseline", + "fixed": false, + "ownerKind": "background-relay", + "input": "reset-without-completion", + "resetDelta": -18456 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 14216, + "completedDelta": 14392 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": -12752, + "completedDelta": -13072 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 15720, + "completedDelta": 14440 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": -12752, + "completedDelta": -13072 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 17312, + "completedDelta": 15656 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -12816, + "completedDelta": -13072 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 14192, + "completedDelta": 14480 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -13072, + "completedDelta": -13072 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 14320, + "completedDelta": 14320 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -13072, + "completedDelta": -13072 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 145808, + "completedDelta": 14224 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 19824, + "completedDelta": -13072 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "scanner", + "input": "reset-without-completion", + "resetDelta": -1152 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 54632, + "completedDelta": 53480 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": -32440, + "completedDelta": -32760 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 36872, + "completedDelta": 35592 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": -32440, + "completedDelta": -32760 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 36616, + "completedDelta": 35592 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -32504, + "completedDelta": -32760 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 35592, + "completedDelta": 35592 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -32760, + "completedDelta": -32760 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 38712, + "completedDelta": 38712 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -32760, + "completedDelta": -32760 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 187152, + "completedDelta": 55568 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 1944, + "completedDelta": -30952 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "title-tracker", + "input": "reset-without-completion", + "resetDelta": 6848 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 103504, + "completedDelta": 101840 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 6208, + "completedDelta": 5568 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 91848, + "completedDelta": 91968 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 6128, + "completedDelta": 5488 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 87216, + "completedDelta": 85104 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": 6064, + "completedDelta": 5488 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 86064, + "completedDelta": 89568 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": 5808, + "completedDelta": 5488 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 79544, + "completedDelta": 79544 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": 216, + "completedDelta": 216 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 218464, + "completedDelta": 85600 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 39520, + "completedDelta": 6304 + }, + { + "variant": "candidate-buffer", + "fixed": true, + "ownerKind": "background-relay", + "input": "reset-without-completion", + "resetDelta": -19024 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 16728, + "completedDelta": 16568 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": -12752, + "completedDelta": -13072 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 15472, + "completedDelta": 14192 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": -12752, + "completedDelta": -13072 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 15216, + "completedDelta": 14192 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -12816, + "completedDelta": -13072 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 14192, + "completedDelta": 14192 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -13072, + "completedDelta": -13072 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 14256, + "completedDelta": 14256 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -13072, + "completedDelta": -13072 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 155680, + "completedDelta": 24096 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 19824, + "completedDelta": -13072 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "scanner", + "input": "reset-without-completion", + "resetDelta": -1152 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 43472, + "completedDelta": 43672 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": -32440, + "completedDelta": -32760 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 36872, + "completedDelta": 35592 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": -32440, + "completedDelta": -32760 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 36616, + "completedDelta": 35592 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": -32504, + "completedDelta": -32760 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 35592, + "completedDelta": 35592 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": -32760, + "completedDelta": -32760 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 38632, + "completedDelta": 38632 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": -25984, + "completedDelta": -25984 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 179200, + "completedDelta": 48312 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 136, + "completedDelta": -32760 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "title-tracker", + "input": "reset-without-completion", + "resetDelta": 4608 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 22, + "retainedDelta": 92824, + "completedDelta": 90264 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-prompt-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 22, + "retainedDelta": 6128, + "completedDelta": 8184 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 23, + "retainedDelta": 87512, + "completedDelta": 84952 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 23, + "retainedDelta": 6128, + "completedDelta": 5488 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 9, + "retainedDelta": 87152, + "completedDelta": 84936 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "short-standard-finished-partial", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 9, + "retainedDelta": 6064, + "completedDelta": 5488 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 24, + "retainedDelta": 90856, + "completedDelta": 89656 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "captured-fish-command-complete", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 24, + "retainedDelta": 5808, + "completedDelta": 5488 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 15, + "retainedDelta": 79544, + "completedDelta": 79544 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "no-escape", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 15, + "retainedDelta": 216, + "completedDelta": 216 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 65536, + "owners": 32, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 218464, + "completedDelta": 85600 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "oversized-incomplete-protocol", + "inputCodeUnits": 1048576, + "owners": 8, + "inputSuffixCodeUnits": 5006, + "retainedDelta": 39520, + "completedDelta": 6304 + }, + { + "variant": "candidate-fallback", + "fixed": true, + "ownerKind": "background-relay", + "input": "reset-without-completion", + "resetDelta": -31584 + } + ] +} diff --git a/docs/audits/osc133-carry-retention/reproduce.cjs b/docs/audits/osc133-carry-retention/reproduce.cjs new file mode 100644 index 00000000000..faccd954dfc --- /dev/null +++ b/docs/audits/osc133-carry-retention/reproduce.cjs @@ -0,0 +1,142 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { load, loadSources, readText, sha, versions: sourceVersions } = require('./sources.cjs') +const { inputs, heap, makeOwner, behavior } = require('./scenario.cjs') + +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function', 'Run with --expose-gc') + +async function main() { + const reports = [] + const versions = [] + for (const variant of ['baseline', 'candidate-buffer', 'candidate-fallback']) { + const fixed = variant !== 'baseline' + const loaded = await load(fixed) + loaded.api.resetOwnRetainedStringCopier() + const originalBuffer = globalThis.Buffer + try { + if (variant === 'candidate-fallback') { + globalThis.Buffer = undefined + } + assert.equal( + loaded.api.ownRetainedString('prefix-\ud800a\udfff\u0000漢-suffix'), + 'prefix-\ud800a\udfff\u0000漢-suffix' + ) + } finally { + globalThis.Buffer = originalBuffer + } + const controls = behavior(loaded.api) + versions.push({ + variant, + fixed, + sourceSha256: loaded.sourceSha256, + bundleSha256: loaded.bundleSha256, + evaluatedSources: loaded.evaluatedSources, + callerSourceHashes: loaded.callerSourceHashes, + controls + }) + for (const ownerKind of ['scanner', 'title-tracker', 'background-relay']) { + for (const input of inputs) { + for (const [chars, count] of [ + [64 * 1024, 32], + [1024 * 1024, 8] + ]) { + const before = await heap() + const owners = Array.from({ length: count }, (_, index) => + makeOwner(loaded.api, ownerKind, input, chars, index) + ) + const retainedDelta = (await heap()) - before + const expectParent = !fixed && input.retained + assert.ok( + expectParent ? retainedDelta > chars * count * 0.75 : retainedDelta < 1024 * 1024, + JSON.stringify({ fixed, ownerKind, input: input.name, retainedDelta }) + ) + for (const owner of owners) { + owner.complete() + } + const completedDelta = (await heap()) - before + assert.ok( + completedDelta < 1024 * 1024, + JSON.stringify({ fixed, ownerKind, input: input.name, completedDelta }) + ) + for (const owner of owners) { + owner.release() + } + reports.push({ + variant, + fixed, + ownerKind, + input: input.name, + inputCodeUnits: chars, + owners: count, + inputSuffixCodeUnits: input.suffix.length, + retainedDelta, + completedDelta + }) + } + } + const input = inputs[0] + const before = await heap() + const owners = Array.from({ length: 8 }, (_, index) => + makeOwner(loaded.api, ownerKind, input, 1024 * 1024, index) + ) + for (const owner of owners) { + owner.release() + } + const resetDelta = (await heap()) - before + assert.ok(resetDelta < 1024 * 1024, JSON.stringify({ fixed, ownerKind, resetDelta })) + reports.push({ variant, fixed, ownerKind, input: 'reset-without-completion', resetDelta }) + } + } + assert.deepEqual(versions[0].controls, versions[1].controls) + assert.deepEqual(versions[0].controls, versions[2].controls) + let crlfReads = 0 + const crlfSources = loadSources((file) => { + crlfReads += 1 + return readText(file).replaceAll('\n', '\r\n') + }) + assert.deepEqual(crlfSources, loadSources()) + assert.equal(crlfReads, 2) + const artifacts = [ + 'sources.cjs', + 'scenario.cjs', + 'reproduce.cjs', + 'source-versions.json', + 'fix.patch', + 'before.config.mjs' + ] + const artifactHashes = Object.fromEntries( + artifacts.map((file) => [file, sha(readText(path.join(__dirname, file)))]) + ) + const result = { + scope: + 'Actual-source bounded diagnostic; captured fish sequence syntax with synthetic large-prefix placement and chunk split. No affected-host, RSS, native PTY, or whole-release claim.', + node: process.version, + electron: process.versions.electron ?? null, + v8: process.versions.v8, + sourcePath: sourceVersions.sourcePath, + crlfReads, + artifactHashes, + versions, + reports + } + const resultPath = process.argv[2] + ? path.resolve(process.argv[2]) + : path.join( + __dirname, + process.versions.electron ? 'electron-results.json' : 'node-results.json' + ) + fs.writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`) + console.log( + JSON.stringify({ resultPath, cases: reports.length, variants: versions.map((x) => x.variant) }) + ) +} +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) +setTimeout(() => { + console.error('fixture deadline') + process.exit(2) +}, 60000).unref() diff --git a/docs/audits/osc133-carry-retention/scenario.cjs b/docs/audits/osc133-carry-retention/scenario.cjs new file mode 100644 index 00000000000..39922432fdd --- /dev/null +++ b/docs/audits/osc133-carry-retention/scenario.cjs @@ -0,0 +1,99 @@ +const assert = require('node:assert/strict') + +const inputs = [ + { name: 'captured-fish-prompt-partial', suffix: '\x1b]133;A;click_events=1', retained: true }, + { name: 'captured-fish-command-partial', suffix: '\x1b]133;C;cmdline_url=npx', retained: true }, + { name: 'short-standard-finished-partial', suffix: '\x1b]133;D;0', retained: false }, + { + name: 'captured-fish-command-complete', + suffix: '\x1b]133;C;cmdline_url=npx\x07', + retained: false + }, + { name: 'no-escape', suffix: 'ordinary output', retained: false }, + { name: 'oversized-incomplete-protocol', suffix: `\x1b]133;${'x'.repeat(5000)}`, retained: true } +] + +async function heap() { + ;/(?:)/.test('') + for (let round = 0; round < 4; round++) { + await new Promise((resolve) => setImmediate(resolve)) + global.gc() + } + return process.memoryUsage().heapUsed +} + +function makeOwner(api, ownerKind, input, chars, index) { + const prefix = `${index}:` + const data = prefix + 'x'.repeat(chars - prefix.length - input.suffix.length) + input.suffix + if (ownerKind === 'scanner') { + const scanner = api.createOsc133CommandFinishedScanner(() => {}) + scanner.scan(data) + return { complete: () => scanner.scan('\x07'), release: () => scanner.reset() } + } + if (ownerKind === 'title-tracker') { + const tracker = api.createTerminalTitleTracker({ onCommandFinished: () => {} }) + tracker.handleChunk(data, { titleScanData: '' }) + return { + complete: () => tracker.handleChunk('\x07', { titleScanData: '' }), + release: () => tracker.dispose() + } + } + const relay = new api.BackgroundTransientFactRelay(() => {}) + relay.setSessionBackground('fixture-session', true) + relay.onSessionData('fixture-session', data) + return { + complete: () => relay.onSessionData('fixture-session', '\x07'), + release: () => relay.onSessionExit('fixture-session') + } +} + +function behavior(api) { + const emitted = [] + const scanner = api.createOsc133CommandFinishedScanner( + (code) => emitted.push(['finished', code]), + () => emitted.push(['started']) + ) + for (const chunk of [ + '\x1b]133;A;click_events=1', + '\x07', + '\x1b]133;C;cmdline_url=npx', + '\x07', + '\x1b]133;D;13', + '7\x1b', + '\\', + '\x1b]133;D;0\x07', + '\x1b]133;D;not-a-number\x07' + ]) { + scanner.scan(chunk) + } + scanner.scan('\x1b]133;D;1234567890') + scanner.reset() + scanner.scan('\x07') + assert.deepEqual(emitted, [['started'], ['finished', 137], ['finished', 0], ['finished', null]]) + const facts = [] + const relay = new api.BackgroundTransientFactRelay((id, fact) => facts.push([id, fact])) + relay.setSessionBackground('s', true) + relay.onSessionData('s', '\x1b]133;D;137') + relay.onSessionData('s', '\x07') + relay.onSessionData('s', '\x1b]133;D;22') + relay.setSessionBackground('s', false) + relay.setSessionBackground('s', true) + relay.onSessionData('s', '\x07') + relay.dispose() + assert.deepEqual(facts[0], ['s', { kind: 'command-finished', exitCode: 137 }]) + assert.equal(facts.filter(([, fact]) => fact.kind === 'command-finished').length, 1) + const utf16 = '\x1b]133;D;1234567890;\ud800a\udfff\u0000漢' + assert.equal(api.ownRetainedString(utf16), utf16) + const splitResults = [] + for (let cut = 1; cut < utf16.length; cut++) { + const values = [] + const split = api.createOsc133CommandFinishedScanner((code) => values.push(code)) + split.scan(utf16.slice(0, cut)) + split.scan(`${utf16.slice(cut)}\x1b\\`) + assert.deepEqual(values, [1234567890]) + splitResults.push(values) + } + return { emitted, facts, splitResults } +} + +module.exports = { inputs, heap, makeOwner, behavior } diff --git a/docs/audits/osc133-carry-retention/source-versions.json b/docs/audits/osc133-carry-retention/source-versions.json new file mode 100644 index 00000000000..5475bbf6302 --- /dev/null +++ b/docs/audits/osc133-carry-retention/source-versions.json @@ -0,0 +1,646 @@ +{ + "sourcePath": "src/shared/terminal-osc133-command-finished.ts", + "baselineSha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd", + "fixedSha256": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "sourceHashLineEndings": "canonical LF", + "dependencies": { + "src/shared/terminal-osc133-command-finished.ts": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0", + "src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "src/shared/terminal-bell-detector.ts": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "src/shared/terminal-color-scheme-protocol.ts": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "src/shared/github/links.ts": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "src/shared/terminal-github-pr-link-detector.ts": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "src/shared/terminal-output-side-effects.ts": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49", + "src/main/daemon/daemon-background-transient-facts.ts": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2" + }, + "callerHashes": [ + { + "path": "src/main/daemon/daemon-stream-data-batcher.ts", + "sha256": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe", + "acceptedSha256": [ + "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe", + "958a01e69c8e24f6eeebe44d0ba0e4e3fe50cb27e7c2302ee8582827633ee26f" + ] + }, + { + "path": "src/main/daemon/daemon-terminal-admission.ts", + "sha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "acceptedSha256": ["14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251"] + }, + { + "path": "src/main/daemon/pty-subprocess/subprocess-handle.ts", + "sha256": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "acceptedSha256": [ + "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a", + "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e" + ] + }, + { + "path": "src/main/daemon/session.ts", + "sha256": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338", + "acceptedSha256": [ + "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338", + "fcf79c354e29bf73549b3fdd4d905d431c210699391f45916bd27dbe858f86b4" + ] + }, + { + "path": "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts", + "sha256": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea", + "acceptedSha256": ["3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea"] + }, + { + "path": "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts", + "sha256": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "acceptedSha256": ["107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33"] + }, + { + "path": "src/shared/terminal-mode-2031-final-state.test.ts", + "sha256": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10", + "acceptedSha256": ["42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10"] + } + ], + "namedSourceProvenance": [ + { + "path": "src/main/daemon/daemon-background-transient-facts.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2" + }, + { + "path": "src/main/daemon/daemon-stream-data-batcher.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "958a01e69c8e24f6eeebe44d0ba0e4e3fe50cb27e7c2302ee8582827633ee26f", + "matchesAuditedBefore": false + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "1c90541151cb3b4a2c77b731db1d2da9cf69f19ad3cd6e673555c7f652edc9ba", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe" + }, + { + "path": "src/main/daemon/daemon-terminal-admission.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251" + }, + { + "path": "src/main/daemon/pty-subprocess/subprocess-handle.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e", + "matchesAuditedBefore": false + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "4623b60a0e362bb3cf218787573966aa056ee5fd1bdefc39fb5293446e4af70b", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a" + }, + { + "path": "src/main/daemon/session.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "fcf79c354e29bf73549b3fdd4d905d431c210699391f45916bd27dbe858f86b4", + "matchesAuditedBefore": false + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "fcf79c354e29bf73549b3fdd4d905d431c210699391f45916bd27dbe858f86b4", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338" + }, + { + "path": "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "afee7baf9568d05298c7a3b7b25057130f6ef21555f2e9079f6fa0bcef8f0084", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea" + }, + { + "path": "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33" + }, + { + "path": "src/shared/agent-detection.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651" + }, + { + "path": "src/shared/agent-name-token-match.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8" + }, + { + "path": "src/shared/agent-title-core.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d" + }, + { + "path": "src/shared/agent-title-decoration.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41" + }, + { + "path": "src/shared/agent-title-evidence.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "792a16e01191e3659e487352b21e6df8898db6cd2ab57c1363f3ec3ea1fd49fa", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea" + }, + { + "path": "src/shared/agent-title-identity.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "c7876bbf40e0e14676f9829e9b9800baa527fe9f5d63ea7f721e293405ea18f9", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453" + }, + { + "path": "src/shared/agent-title-status.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "8df0706f4074d06909d1264e22203f431a09118e056e934b96758d963a69f1bd", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb" + }, + { + "path": "src/shared/github/links.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7" + }, + { + "path": "src/shared/opencode-terminal-title.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec" + }, + { + "path": "src/shared/osc-title-extraction.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a" + }, + { + "path": "src/shared/own-retained-string.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": null, + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2" + }, + { + "path": "src/shared/owned-utf16-suffix.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + { + "path": "src/shared/pane-agent-evidence-sources.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f" + }, + { + "path": "src/shared/pane-agent-identity-adapter.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9" + }, + { + "path": "src/shared/pi-compatible-synthetic-title.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "2c43b6aa0b26f328bc7d51bfc8b4f7a8937156f91b430ccedc945827808e188d", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f" + }, + { + "path": "src/shared/pi-state-title-marker.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "5cccf1bb0e00d362e9a824996a6755cf101c71a895413e6862c7a3d68e8828af", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8" + }, + { + "path": "src/shared/shell-process-detection.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f" + }, + { + "path": "src/shared/synthetic-agent-title.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "8944067df16920a6ed068a251d6cf4c270263db68e9b489708d0ae467ae9326c", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922" + }, + { + "path": "src/shared/terminal-bell-detector.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05" + }, + { + "path": "src/shared/terminal-color-scheme-protocol.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700" + }, + { + "path": "src/shared/terminal-github-pr-link-detector.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e" + }, + { + "path": "src/shared/terminal-mode-2031-final-state.test.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10" + }, + { + "path": "src/shared/terminal-osc133-command-finished.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd" + }, + { + "path": "src/shared/terminal-output-side-effects.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "c1666a54339ece63e4180ab7e3eceec244f5bdd686c6dc3a0eed6e9ab93abcd4", + "matchesAuditedBefore": false + } + }, + "auditedBeforeSha256": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49" + }, + { + "path": "src/shared/terminal-title-agent-type.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864" + }, + { + "path": "src/shared/terminal-title-classification-memo.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda" + }, + { + "path": "src/shared/terminal-title-wrapper-segments.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78" + }, + { + "path": "src/shared/tui-agent-display-names.ts", + "namedRefs": { + "main291b": { + "commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sha256": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "matchesAuditedBefore": true + }, + "v1.4.198": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sha256": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296", + "matchesAuditedBefore": true + } + }, + "auditedBeforeSha256": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296" + } + ], + "historicalScope": "Scanner baseline is identical at main291b and v1.4.198. Current caller/helper dependencies are evaluated; own-retained-string is absent in v1.4.198. This is not a complete historical-release replay.", + "callerScope": "Non-evaluated supporting caller provenance accepts only recorded audited-before or named main291b bytes; each runtime report records the actual selected hash. Evaluated bundle dependencies require the single fixed hash." +} diff --git a/docs/audits/osc133-carry-retention/sources.cjs b/docs/audits/osc133-carry-retention/sources.cjs new file mode 100644 index 00000000000..6bfa507dfce --- /dev/null +++ b/docs/audits/osc133-carry-retention/sources.cjs @@ -0,0 +1,87 @@ +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 { build } = 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 scanner drift') + const patches = parsePatch(canonical(read(path.join(__dirname, 'fix.patch')))) + assert.equal(patches.length, 1) + assert.equal(patches[0].newFileName, `b/${versions.sourcePath}`) + const baseline = applyPatch(fixed, reversePatch(patches[0])) + assert.notEqual(baseline, false) + assert.equal(sha(baseline), versions.baselineSha256, 'Baseline scanner drift') + return { baseline, fixed } +} + +async function load(fixed) { + const sources = loadSources() + const callerSourceHashes = {} + for (const caller of versions.callerHashes) { + const actual = sha(readText(path.join(root, caller.path))) + assert.ok(caller.acceptedSha256.includes(actual), `Caller drift: ${caller.path}`) + callerSourceHashes[caller.path] = actual + } + const evaluatedSources = {} + const built = await build({ + stdin: { + contents: [ + "export { createOsc133CommandFinishedScanner } from './src/shared/terminal-osc133-command-finished'", + "export { BackgroundTransientFactRelay } from './src/main/daemon/daemon-background-transient-facts'", + "export { createTerminalTitleTracker } from './src/shared/terminal-output-side-effects'", + "export { ownRetainedString, resetOwnRetainedStringCopier } from './src/shared/own-retained-string'" + ].join('\n'), + resolveDir: root, + loader: 'ts' + }, + absWorkingDir: root, + bundle: true, + platform: 'node', + format: 'cjs', + write: false, + plugins: [ + { + name: 'hash-fenced-osc133-carry', + setup(builder) { + builder.onLoad({ filter: /\.ts$/ }, ({ path: filename }) => { + const relative = path.relative(root, filename).split(path.sep).join('/') + const expected = versions.dependencies[relative] + assert.ok(expected, `Unreviewed dependency: ${relative}`) + let contents = readText(filename) + assert.equal(sha(contents), expected, `Dependency drift: ${relative}`) + if (relative === versions.sourcePath) { + contents = fixed ? sources.fixed : sources.baseline + } + evaluatedSources[relative] = sha(contents) + return { contents, loader: 'ts' } + }) + } + } + ] + }) + assert.deepEqual(Object.keys(evaluatedSources).sort(), Object.keys(versions.dependencies).sort()) + const filename = path.join(__dirname, fixed ? 'fixed-bundle.cjs' : 'baseline-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, + sourceSha256: sha(fixed ? sources.fixed : sources.baseline), + bundleSha256: sha(built.outputFiles[0].text), + evaluatedSources, + callerSourceHashes + } +} + +module.exports = { load, loadSources, readText, root, sha, versions } diff --git a/docs/audits/osc133-carry-retention/validation.json b/docs/audits/osc133-carry-retention/validation.json new file mode 100644 index 00000000000..50d981a6b57 --- /dev/null +++ b/docs/audits/osc133-carry-retention/validation.json @@ -0,0 +1,83 @@ +{ + "backgroundLaunch": "All tests and proofs used ORCA_BACKGROUND_LAUNCH=1; Electron only ran with ELECTRON_RUN_AS_NODE=1. No app or native PTY.", + "fixedTests": { + "passed": 45, + "failed": 0, + "files": 4, + "newTests": 8, + "config": "config/vitest.config.ts" + }, + "baselineOverlay": { + "passed": 41, + "failed": 4, + "config": "docs/audits/osc133-carry-retention/before.config.mjs", + "intendedFailures": [ + { + "test": "OSC 133 carry with Bufferless copying=false owns a retained fish suffix \"\\u001b]133;A;click_events=1\"", + "assertion": "AssertionError: expected 33560392 to be less than 2097152", + "retainedBytes": 33560392 + }, + { + "test": "OSC 133 carry with Bufferless copying=false owns a retained fish suffix \"\\u001b]133;C;cmdline_url=npx\"", + "assertion": "AssertionError: expected 33565360 to be less than 2097152", + "retainedBytes": 33565360 + }, + { + "test": "OSC 133 carry with Bufferless copying=true owns a retained fish suffix \"\\u001b]133;A;click_events=1\"", + "assertion": "AssertionError: expected 33557160 to be less than 2097152", + "retainedBytes": 33557160 + }, + { + "test": "OSC 133 carry with Bufferless copying=true owns a retained fish suffix \"\\u001b]133;C;cmdline_url=npx\"", + "assertion": "AssertionError: expected 33550680 to be less than 2097152", + "retainedBytes": 33550680 + } + ] + }, + "portableProofs": { + "nodeCases": 117, + "electronCases": 117, + "crlfSourceAndPatchReads": 2, + "evaluatedModules": 28, + "nonEvaluatedCallerFiles": 7, + "variants": ["baseline", "fixed Buffer copier", "fixed Bufferless copier"] + }, + "typechecks": { + "node": "Passed full Node project; parent root rerun after resolving its concurrent viewport-test typing.", + "web": "Passed full Web project in parent root shared desktop run.", + "cli": "Passed full CLI project in parent root shared desktop run." + }, + "fullPublicationQuality": { + "paths": [ + "src/shared/terminal-osc133-command-finished.ts", + "src/shared/terminal-osc133-carry-retention.test.ts", + "docs/audits/osc133-carry-retention/sources.cjs", + "docs/audits/osc133-carry-retention/scenario.cjs", + "docs/audits/osc133-carry-retention/reproduce.cjs", + "docs/audits/osc133-carry-retention/before.config.mjs" + ], + "scans": [ + "default rules and unused suppression", + "casting", + "type-aware", + "React Doctor", + "design system" + ], + "result": "All five full-file scans passed with --deny-warnings, including CJS/MJS artifact files." + }, + "changedQuality": { + "base": "4a09b1d108cfd8b57ffcc5d727b3a3bd71ae71fb", + "result": "Passed all five scans plus SAFETY rationale gate across six concurrent changed files; artifacts separately covered by explicit full-file scans." + }, + "productHashes": [ + { + "path": "src/shared/terminal-osc133-command-finished.ts", + "sha256": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0" + }, + { + "path": "src/shared/terminal-osc133-carry-retention.test.ts", + "sha256": "f123ddacc2d395f5896dc91acba13fbd9447f9152d36b8164a11d83271d9279d" + } + ], + "limits": "Synthetic parent size/boundary with captured fish sequence syntax. Heap deltas are not RSS. No historical whole-app or incident attribution. Baseline overlay retains current dependency implementations." +} diff --git a/docs/audits/plugin-worker-output-retention/README.md b/docs/audits/plugin-worker-output-retention/README.md new file mode 100644 index 00000000000..8ce1784c854 --- /dev/null +++ b/docs/audits/plugin-worker-output-retention/README.md @@ -0,0 +1,67 @@ +# Plugin worker output retention + +The worker output parser capped a line at 8,192 code units, but retained slices could keep a much larger decoded input chunk alive. This artifact reproduces two ownership paths using the actual parser and actual `PluginLogBuffer`: + +1. An unfinished line stays in the stream listener's buffer. +2. A completed short or truncated line stays in the service's 200-entry log ring. + +The fix uses the existing `ownRetainedString` copier for incomplete segments retained across callbacks and for the bounded string passed to the log sink. Line contents, truncation, callback invocation, ring capacity, and worker lifecycle are unchanged. Strings shorter than 13 code units keep the helper's existing fast path. + +## Production reachability and lifetime + +- `src/main/plugins/plugin-host-process.ts` installs the parser on child stdout and stderr at lines 101–102, with UTF-8 decoding in the parser. The production sink passes through `plugin-worker-manager.ts:148` and `plugin-service.ts:94` to `plugin-log-buffer.ts:14`, which stores the original string without copying it. +- The parser retains at most one incomplete line per stream. The default five active workers allow ten live stdout/stderr buffers. Worker slots are acquired before startup. Idle workers are reaped after five minutes, checked every minute; stream end clears parser buffering. +- The log ring belongs to the long-lived `PluginService`, not the worker. Worker exit and stream end preserve its last 200 entries per plugin. Ring eviction releases the entries. Several lines can share one parent; the backing allocation must be counted once. +- This is a main-process plugin path. The plugin-system setting gates activation (`src/main/startup/main-process-plugins.ts:59–62`). It is not a terminal daemon or renderer retention path. The plugin `orca.log` IPC message is a separate producer. +- `PluginService.getLogs` and its IPC handler expose the existing ring. Reading or serializing a concatenated string can flatten it and shorten its parent retention, but does not remove the service's ring entries. + +## Reproduce + +From the repository root with the project's dependencies installed: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/plugin-worker-output-retention/reproduce.cjs +``` + +For Electron, run the installed Electron executable with `ELECTRON_RUN_AS_NODE=1`, `ORCA_BACKGROUND_LAUNCH=1`, and the same arguments. This uses Node mode without opening an app window. For example, on macOS: + +```sh +ORCA_BACKGROUND_LAUNCH=1 ELECTRON_RUN_AS_NODE=1 node_modules/electron/dist/Electron.app/Contents/MacOS/Electron --expose-gc --max-old-space-size=192 docs/audits/plugin-worker-output-retention/reproduce.cjs +``` + +The runner writes `node-results.json` or `electron-results.json` beside itself. Pass `--output ` to preserve the captured reports. It uses inert PassThrough streams, no OS child process or network, a 192 MiB heap limit, and a 30-second deadline. + +`sources.cjs` reverses `fix.patch` in memory and checks exact baseline/fixed parser hashes. It also checks eight dependency/caller hashes and records actual evaluated source and bundle hashes. No source files are overwritten. A synthetic CRLF read control checks all ten source/patch reads. + +`source-versions.json` records identical parser, sink, caller, and helper hashes at main checkpoint `291b4ddd6f1c1af480169885e0fda7f9c78ff053`, main `f78483ec29891ab11f49bb25e6cd628837b1242e`, and the #20960 topic `np-oom-scan-retained-text-slices` at `0d2efbc0d3b4f902b9bc02f5451c6ff4291405cf`. The parser, sink and caller modules also match v1.4.198 (`e0826956fcfc532f5a1e55b5e081f2e57e553c43`), which lacks the newer `own-retained-string.ts` wrapper. The baseline bundle uses only the unchanged parser and ring; fixed variants use the recorded publication helper. These are source controls with current build dependencies, not a historical app binary. + +## Controls and results + +Both Node 26.6 and Electron 43.7 / Node 24.21 pass 24 cases: baseline, diagnostic tail-only copy, fixed Buffer copy, and fixed code-unit-copy fallback, each with two input sizes and three ownership cases. The fallback is a shared-helper compatibility control; production main normally has Buffer. + +| Retained owner | Baseline heap delta | Fixed heap delta | +| ----------------------------------------------- | ------------------: | ---------------: | +| Ten unfinished tails, 64 KiB input each | 0.72–0.73 MB | 9–24 KB | +| Eight unfinished tails, 4 MiB input each | 33.56–33.57 MB | 6–11 KB | +| 200 short log rows from 205 × 64 KiB inputs | 13.14–13.16 MB | 27–45 KB | +| 200 truncated log rows from 205 × 64 KiB inputs | 13.14–13.15 MB | 3.29–3.31 MB | +| Eight short log rows, 4 MiB input each | 33.56 MB | about 1 KB | +| Eight truncated log rows, 4 MiB input each | 33.56 MB | 128–132 KB | + +Heap deltas include GC noise. Truncated strings legitimately retain 8,192 code units, including the non-ASCII truncation suffix. Tail-only copying fixes unfinished buffers but leaves both log-ring paths. Stream end clears no-op-sink tails while the actual ring remains live; replacing all 200 entries releases the original parents. + +64 KiB is an ordinary-scale stdio input control. The 4 MiB input is amplified stress, not a claim about normal OS pipe reads. PassThrough delivers the selected chunk intact; real child-pipe chunk sizes depend on runtime and OS. Retention is bounded by owner count, ring capacity and backing input size; this is not an unbounded line queue. + +Behavior comparisons cover blank and split lines, null/empty streams, CRLF, end flushing, discard/resume after overflow, log level, exact ring content, NUL, lone surrogates, emoji, and the code-unit limit. Value comparisons run separately from heap controls because comparing concatenated strings can flatten them and change retention. + +## Validation + +```sh +ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/plugins/plugin-worker-output-retention.test.ts src/main/plugins/plugin-worker-output-buffer.test.ts src/main/plugins/plugin-host-process.test.ts src/shared/own-retained-string.test.ts +ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/plugin-worker-output-retention/before.config.mjs src/main/plugins/plugin-worker-output-retention.test.ts src/main/plugins/plugin-worker-output-buffer.test.ts +ORCA_BACKGROUND_LAUNCH=1 pnpm tc:node +``` + +The fixed source passes 20 tests. The baseline overlay intentionally fails all three new heap regressions: approximately 33.6 MB for unfinished tails and 13.2 MB for each ring case, against 2 MiB and 5 MiB ceilings; its original behavior test passes. Node typecheck passes. All five changed-quality scan configurations pass over all five product/test/artifact code files with `--no-ignore --deny-warnings`, including the ordinary and type-aware lint rules. + +This proves a reachable code mechanism and its repair. It does not establish affected-host plugin use, output cadence, aggregate app RSS, or attribution to #19831 or another incident. diff --git a/docs/audits/plugin-worker-output-retention/before.config.mjs b/docs/audits/plugin-worker-output-retention/before.config.mjs new file mode 100644 index 00000000000..607bce497cf --- /dev/null +++ b/docs/audits/plugin-worker-output-retention/before.config.mjs @@ -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/plugins/plugin-worker-output-buffer.ts') + +export default mergeConfig( + baseConfig, + defineConfig({ + plugins: [ + { + name: 'plugin-output-before-fix', + enforce: 'pre', + transform(_code, id) { + return resolve(id.split('?')[0]) === sourcePath ? { code: before, map: null } : undefined + } + } + ] + }) +) diff --git a/docs/audits/plugin-worker-output-retention/electron-results.json b/docs/audits/plugin-worker-output-retention/electron-results.json new file mode 100644 index 00000000000..2a484e31dd2 --- /dev/null +++ b/docs/audits/plugin-worker-output-retention/electron-results.json @@ -0,0 +1,493 @@ +{ + "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" + }, + "artifactHashes": { + "reproduce.cjs": "1fe840d9b4ecc76c42cc2e8bcb87c54db78a91510819f755403a01ecb49181da", + "sources.cjs": "8390b117b07ff8c0638e02183624d8da71b32cea955a366e16c80701dd9c38b1", + "source-versions.json": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "fix.patch": "ec28362681138dbc311e1c1a14154c250f418423bbb3d30cc75adfc46bbb5d57" + }, + "crlfLoaderControl": { + "reads": 10, + "identical": true + }, + "bundles": { + "before": { + "checkedSources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "evaluatedSources": { + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914" + }, + "sourceVersionsSha256": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "bundleSha256": "c44c04feb9f55a1fb05da64a8bd34c4bbb92d292d3f070062705d8b09dfc34a0" + }, + "tail-only": { + "checkedSources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/plugins/plugin-worker-output-buffer.ts": "5d68af2f66e1466b5a642fa9bb301b8f04ccd2c2741dd5731faac7a2f30c5be5", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "sourceVersionsSha256": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "bundleSha256": "12a2b78410879874c0f6f36af3f6463a68d38c93ce4a098a87bffc24f5ac98b0" + }, + "fixed-buffer": { + "checkedSources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "sourceVersionsSha256": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "bundleSha256": "c1b3988c001cf1545164a1cab25cf152af5eb366adae5ae11deb1a5b469a2162" + }, + "fixed-fallback": { + "checkedSources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "sourceVersionsSha256": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "bundleSha256": "c1b3988c001cf1545164a1cab25cf152af5eb366adae5ae11deb1a5b469a2162" + } + }, + "behaviors": { + "before": { + "lines": [ + ["error", "hello world"], + [ + "error", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx… [truncated]" + ], + ["error", "ok"], + ["error", "retained-output-tail"] + ], + "unicode": [ + ["info", "short"], + ["info", "twelve chars"], + ["info", "\ud800a\udfff\u0000漢"], + [ + "info", + "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + ], + [ + "info", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… [truncated]" + ], + [ + "info", + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq… [truncated]" + ], + ["info", "next\r"], + ["info", "unterminated 😀"] + ] + }, + "tail-only": { + "lines": [ + ["error", "hello world"], + [ + "error", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx… [truncated]" + ], + ["error", "ok"], + ["error", "retained-output-tail"] + ], + "unicode": [ + ["info", "short"], + ["info", "twelve chars"], + ["info", "\ud800a\udfff\u0000漢"], + [ + "info", + "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + ], + [ + "info", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… [truncated]" + ], + [ + "info", + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq… [truncated]" + ], + ["info", "next\r"], + ["info", "unterminated 😀"] + ] + }, + "fixed-buffer": { + "lines": [ + ["error", "hello world"], + [ + "error", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx… [truncated]" + ], + ["error", "ok"], + ["error", "retained-output-tail"] + ], + "unicode": [ + ["info", "short"], + ["info", "twelve chars"], + ["info", "\ud800a\udfff\u0000漢"], + [ + "info", + "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + ], + [ + "info", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… [truncated]" + ], + [ + "info", + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq… [truncated]" + ], + ["info", "next\r"], + ["info", "unterminated 😀"] + ] + }, + "fixed-fallback": { + "lines": [ + ["error", "hello world"], + [ + "error", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx… [truncated]" + ], + ["error", "ok"], + ["error", "retained-output-tail"] + ], + "unicode": [ + ["info", "short"], + ["info", "twelve chars"], + ["info", "\ud800a\udfff\u0000漢"], + [ + "info", + "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + ], + [ + "info", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… [truncated]" + ], + [ + "info", + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq… [truncated]" + ], + ["info", "next\r"], + ["info", "unterminated 😀"] + ] + } + }, + "reports": [ + { + "kind": "tail", + "variant": "before", + "chars": 65536, + "count": 10, + "heldDelta": 716564, + "endedDelta": 122728 + }, + { + "kind": "tail", + "variant": "before", + "chars": 4194304, + "count": 8, + "heldDelta": 33561956, + "endedDelta": 8432 + }, + { + "kind": "ring", + "variant": "before", + "chars": 65536, + "count": 205, + "truncated": false, + "expectedParents": 200, + "heldDelta": 13141604, + "endedDelta": 13141904, + "evictedDelta": 28332 + }, + { + "kind": "ring", + "variant": "before", + "chars": 4194304, + "count": 8, + "truncated": false, + "expectedParents": 8, + "heldDelta": 33555548, + "endedDelta": 33556112, + "evictedDelta": 9188 + }, + { + "kind": "ring", + "variant": "before", + "chars": 65536, + "count": 205, + "truncated": true, + "expectedParents": 200, + "heldDelta": 13135672, + "endedDelta": 13135728, + "evictedDelta": 21692 + }, + { + "kind": "ring", + "variant": "before", + "chars": 4194304, + "count": 8, + "truncated": true, + "expectedParents": 8, + "heldDelta": 33555300, + "endedDelta": 33555356, + "evictedDelta": 5060 + }, + { + "kind": "tail", + "variant": "tail-only", + "chars": 65536, + "count": 10, + "heldDelta": 7664, + "endedDelta": 8548 + }, + { + "kind": "tail", + "variant": "tail-only", + "chars": 4194304, + "count": 8, + "heldDelta": 6068, + "endedDelta": 6924 + }, + { + "kind": "ring", + "variant": "tail-only", + "chars": 65536, + "count": 205, + "truncated": false, + "expectedParents": 200, + "heldDelta": 13137012, + "endedDelta": 13137068, + "evictedDelta": 23480 + }, + { + "kind": "ring", + "variant": "tail-only", + "chars": 4194304, + "count": 8, + "truncated": false, + "expectedParents": 8, + "heldDelta": 33555144, + "endedDelta": 33555200, + "evictedDelta": 8276 + }, + { + "kind": "ring", + "variant": "tail-only", + "chars": 65536, + "count": 205, + "truncated": true, + "expectedParents": 200, + "heldDelta": 13128492, + "endedDelta": 13128548, + "evictedDelta": 19872 + }, + { + "kind": "ring", + "variant": "tail-only", + "chars": 4194304, + "count": 8, + "truncated": true, + "expectedParents": 8, + "heldDelta": 33555236, + "endedDelta": 33555292, + "evictedDelta": 8208 + }, + { + "kind": "tail", + "variant": "fixed-buffer", + "chars": 65536, + "count": 10, + "heldDelta": 8944, + "endedDelta": 14304 + }, + { + "kind": "tail", + "variant": "fixed-buffer", + "chars": 4194304, + "count": 8, + "heldDelta": 10076, + "endedDelta": 9740 + }, + { + "kind": "ring", + "variant": "fixed-buffer", + "chars": 65536, + "count": 205, + "truncated": false, + "expectedParents": 200, + "heldDelta": 26624, + "endedDelta": 26680, + "evictedDelta": 18692 + }, + { + "kind": "ring", + "variant": "fixed-buffer", + "chars": 4194304, + "count": 8, + "truncated": false, + "expectedParents": 8, + "heldDelta": 776, + "endedDelta": 832, + "evictedDelta": 8276 + }, + { + "kind": "ring", + "variant": "fixed-buffer", + "chars": 65536, + "count": 205, + "truncated": true, + "expectedParents": 200, + "heldDelta": 3296796, + "endedDelta": 3296852, + "evictedDelta": 20952 + }, + { + "kind": "ring", + "variant": "fixed-buffer", + "chars": 4194304, + "count": 8, + "truncated": true, + "expectedParents": 8, + "heldDelta": 131556, + "endedDelta": 131612, + "evictedDelta": 8208 + }, + { + "kind": "tail", + "variant": "fixed-fallback", + "chars": 65536, + "count": 10, + "heldDelta": 8944, + "endedDelta": 8596 + }, + { + "kind": "tail", + "variant": "fixed-fallback", + "chars": 4194304, + "count": 8, + "heldDelta": 6068, + "endedDelta": 10048 + }, + { + "kind": "ring", + "variant": "fixed-fallback", + "chars": 65536, + "count": 205, + "truncated": false, + "expectedParents": 200, + "heldDelta": 33472, + "endedDelta": 39568, + "evictedDelta": 31580 + }, + { + "kind": "ring", + "variant": "fixed-fallback", + "chars": 4194304, + "count": 8, + "truncated": false, + "expectedParents": 8, + "heldDelta": 776, + "endedDelta": 832, + "evictedDelta": 8276 + }, + { + "kind": "ring", + "variant": "fixed-fallback", + "chars": 65536, + "count": 205, + "truncated": true, + "expectedParents": 200, + "heldDelta": 3290604, + "endedDelta": 3290660, + "evictedDelta": 14776 + }, + { + "kind": "ring", + "variant": "fixed-fallback", + "chars": 4194304, + "count": 8, + "truncated": true, + "expectedParents": 8, + "heldDelta": 131556, + "endedDelta": 131612, + "evictedDelta": 8208 + } + ] +} diff --git a/docs/audits/plugin-worker-output-retention/fix.patch b/docs/audits/plugin-worker-output-retention/fix.patch new file mode 100644 index 00000000000..06b54c9b1ca --- /dev/null +++ b/docs/audits/plugin-worker-output-retention/fix.patch @@ -0,0 +1,18 @@ +diff --git a/src/main/plugins/plugin-worker-output-buffer.ts b/src/main/plugins/plugin-worker-output-buffer.ts +index 836330c879..6a0cb2a078 100644 +--- a/src/main/plugins/plugin-worker-output-buffer.ts ++++ b/src/main/plugins/plugin-worker-output-buffer.ts +@@ -1,0 +2 @@ import type { Readable } from 'node:stream' ++import { ownRetainedString } from '../../shared/own-retained-string' +@@ -24,3 +25,5 @@ export function pipePluginWorkerOutput( +- truncated +- ? `${line.slice(0, PLUGIN_WORKER_OUTPUT_LINE_LIMIT - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}` +- : line ++ ownRetainedString( ++ truncated ++ ? `${line.slice(0, PLUGIN_WORKER_OUTPUT_LINE_LIMIT - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}` ++ : line ++ ) +@@ -52 +55 @@ export function pipePluginWorkerOutput( +- buffered += segment ++ buffered += newline === -1 ? ownRetainedString(segment) : segment diff --git a/docs/audits/plugin-worker-output-retention/node-results.json b/docs/audits/plugin-worker-output-retention/node-results.json new file mode 100644 index 00000000000..0f4eb1a4d4b --- /dev/null +++ b/docs/audits/plugin-worker-output-retention/node-results.json @@ -0,0 +1,492 @@ +{ + "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" + }, + "artifactHashes": { + "reproduce.cjs": "1fe840d9b4ecc76c42cc2e8bcb87c54db78a91510819f755403a01ecb49181da", + "sources.cjs": "8390b117b07ff8c0638e02183624d8da71b32cea955a366e16c80701dd9c38b1", + "source-versions.json": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "fix.patch": "ec28362681138dbc311e1c1a14154c250f418423bbb3d30cc75adfc46bbb5d57" + }, + "crlfLoaderControl": { + "reads": 10, + "identical": true + }, + "bundles": { + "before": { + "checkedSources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "evaluatedSources": { + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914" + }, + "sourceVersionsSha256": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "bundleSha256": "c44c04feb9f55a1fb05da64a8bd34c4bbb92d292d3f070062705d8b09dfc34a0" + }, + "tail-only": { + "checkedSources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/plugins/plugin-worker-output-buffer.ts": "5d68af2f66e1466b5a642fa9bb301b8f04ccd2c2741dd5731faac7a2f30c5be5", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "sourceVersionsSha256": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "bundleSha256": "12a2b78410879874c0f6f36af3f6463a68d38c93ce4a098a87bffc24f5ac98b0" + }, + "fixed-buffer": { + "checkedSources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "evaluatedSources": { + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "sourceVersionsSha256": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "bundleSha256": "c1b3988c001cf1545164a1cab25cf152af5eb366adae5ae11deb1a5b469a2162" + }, + "fixed-fallback": { + "checkedSources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/plugins/plugin-worker-output-buffer.ts": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "sourceVersionsSha256": "247bf1f8587b37129212582e88b792dc345197666b6ceadaddc12eaa65f84de7", + "bundleSha256": "c1b3988c001cf1545164a1cab25cf152af5eb366adae5ae11deb1a5b469a2162" + } + }, + "behaviors": { + "before": { + "lines": [ + ["error", "hello world"], + [ + "error", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx… [truncated]" + ], + ["error", "ok"], + ["error", "retained-output-tail"] + ], + "unicode": [ + ["info", "short"], + ["info", "twelve chars"], + ["info", "\ud800a\udfff\u0000漢"], + [ + "info", + "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + ], + [ + "info", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… [truncated]" + ], + [ + "info", + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq… [truncated]" + ], + ["info", "next\r"], + ["info", "unterminated 😀"] + ] + }, + "tail-only": { + "lines": [ + ["error", "hello world"], + [ + "error", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx… [truncated]" + ], + ["error", "ok"], + ["error", "retained-output-tail"] + ], + "unicode": [ + ["info", "short"], + ["info", "twelve chars"], + ["info", "\ud800a\udfff\u0000漢"], + [ + "info", + "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + ], + [ + "info", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… [truncated]" + ], + [ + "info", + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq… [truncated]" + ], + ["info", "next\r"], + ["info", "unterminated 😀"] + ] + }, + "fixed-buffer": { + "lines": [ + ["error", "hello world"], + [ + "error", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx… [truncated]" + ], + ["error", "ok"], + ["error", "retained-output-tail"] + ], + "unicode": [ + ["info", "short"], + ["info", "twelve chars"], + ["info", "\ud800a\udfff\u0000漢"], + [ + "info", + "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + ], + [ + "info", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… [truncated]" + ], + [ + "info", + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq… [truncated]" + ], + ["info", "next\r"], + ["info", "unterminated 😀"] + ] + }, + "fixed-fallback": { + "lines": [ + ["error", "hello world"], + [ + "error", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx… [truncated]" + ], + ["error", "ok"], + ["error", "retained-output-tail"] + ], + "unicode": [ + ["info", "short"], + ["info", "twelve chars"], + ["info", "\ud800a\udfff\u0000漢"], + [ + "info", + "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + ], + [ + "info", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… [truncated]" + ], + [ + "info", + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq… [truncated]" + ], + ["info", "next\r"], + ["info", "unterminated 😀"] + ] + } + }, + "reports": [ + { + "kind": "tail", + "variant": "before", + "chars": 65536, + "count": 10, + "heldDelta": 734088, + "endedDelta": 138584 + }, + { + "kind": "tail", + "variant": "before", + "chars": 4194304, + "count": 8, + "heldDelta": 33573744, + "endedDelta": 20328 + }, + { + "kind": "ring", + "variant": "before", + "chars": 65536, + "count": 205, + "truncated": false, + "expectedParents": 200, + "heldDelta": 13157504, + "endedDelta": 13157944, + "evictedDelta": 41424 + }, + { + "kind": "ring", + "variant": "before", + "chars": 4194304, + "count": 8, + "truncated": false, + "expectedParents": 8, + "heldDelta": 33556256, + "endedDelta": 33557296, + "evictedDelta": 16464 + }, + { + "kind": "ring", + "variant": "before", + "chars": 65536, + "count": 205, + "truncated": true, + "expectedParents": 200, + "heldDelta": 13150592, + "endedDelta": 13150688, + "evictedDelta": 31088 + }, + { + "kind": "ring", + "variant": "before", + "chars": 4194304, + "count": 8, + "truncated": true, + "expectedParents": 8, + "heldDelta": 33555760, + "endedDelta": 33555856, + "evictedDelta": 14768 + }, + { + "kind": "tail", + "variant": "tail-only", + "chars": 65536, + "count": 10, + "heldDelta": 22120, + "endedDelta": 29824 + }, + { + "kind": "tail", + "variant": "tail-only", + "chars": 4194304, + "count": 8, + "heldDelta": 11248, + "endedDelta": 12272 + }, + { + "kind": "ring", + "variant": "tail-only", + "chars": 65536, + "count": 205, + "truncated": false, + "expectedParents": 200, + "heldDelta": 13149032, + "endedDelta": 13149128, + "evictedDelta": 32344 + }, + { + "kind": "ring", + "variant": "tail-only", + "chars": 4194304, + "count": 8, + "truncated": false, + "expectedParents": 8, + "heldDelta": 32504472, + "endedDelta": 32504568, + "evictedDelta": -1036264 + }, + { + "kind": "ring", + "variant": "tail-only", + "chars": 65536, + "count": 205, + "truncated": true, + "expectedParents": 200, + "heldDelta": 13139920, + "endedDelta": 13140016, + "evictedDelta": 20016 + }, + { + "kind": "ring", + "variant": "tail-only", + "chars": 4194304, + "count": 8, + "truncated": true, + "expectedParents": 8, + "heldDelta": 33555760, + "endedDelta": 33555856, + "evictedDelta": 14768 + }, + { + "kind": "tail", + "variant": "fixed-buffer", + "chars": 65536, + "count": 10, + "heldDelta": 22392, + "endedDelta": 24808 + }, + { + "kind": "tail", + "variant": "fixed-buffer", + "chars": 4194304, + "count": 8, + "heldDelta": 13120, + "endedDelta": 15808 + }, + { + "kind": "ring", + "variant": "fixed-buffer", + "chars": 65536, + "count": 205, + "truncated": false, + "expectedParents": 200, + "heldDelta": 36832, + "endedDelta": 36928, + "evictedDelta": 27344 + }, + { + "kind": "ring", + "variant": "fixed-buffer", + "chars": 4194304, + "count": 8, + "truncated": false, + "expectedParents": 8, + "heldDelta": 1056, + "endedDelta": 1152, + "evictedDelta": 14752 + }, + { + "kind": "ring", + "variant": "fixed-buffer", + "chars": 65536, + "count": 205, + "truncated": true, + "expectedParents": 200, + "heldDelta": 3305736, + "endedDelta": 3305832, + "evictedDelta": 28928 + }, + { + "kind": "ring", + "variant": "fixed-buffer", + "chars": 4194304, + "count": 8, + "truncated": true, + "expectedParents": 8, + "heldDelta": 127904, + "endedDelta": 128000, + "evictedDelta": 10784 + }, + { + "kind": "tail", + "variant": "fixed-fallback", + "chars": 65536, + "count": 10, + "heldDelta": 14360, + "endedDelta": 19176 + }, + { + "kind": "tail", + "variant": "fixed-fallback", + "chars": 4194304, + "count": 8, + "heldDelta": 11168, + "endedDelta": 17664 + }, + { + "kind": "ring", + "variant": "fixed-fallback", + "chars": 65536, + "count": 205, + "truncated": false, + "expectedParents": 200, + "heldDelta": 36832, + "endedDelta": 36928, + "evictedDelta": 27344 + }, + { + "kind": "ring", + "variant": "fixed-fallback", + "chars": 4194304, + "count": 8, + "truncated": false, + "expectedParents": 8, + "heldDelta": 1056, + "endedDelta": 1152, + "evictedDelta": 14752 + }, + { + "kind": "ring", + "variant": "fixed-fallback", + "chars": 65536, + "count": 205, + "truncated": true, + "expectedParents": 200, + "heldDelta": 3300216, + "endedDelta": 3300312, + "evictedDelta": 23440 + }, + { + "kind": "ring", + "variant": "fixed-fallback", + "chars": 4194304, + "count": 8, + "truncated": true, + "expectedParents": 8, + "heldDelta": 127904, + "endedDelta": 128000, + "evictedDelta": 10784 + } + ] +} diff --git a/docs/audits/plugin-worker-output-retention/reproduce.cjs b/docs/audits/plugin-worker-output-retention/reproduce.cjs new file mode 100644 index 00000000000..7af9cb5fea5 --- /dev/null +++ b/docs/audits/plugin-worker-output-retention/reproduce.cjs @@ -0,0 +1,249 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { PassThrough } = require('node:stream') +const { once, EventEmitter } = require('node:events') +const { load, loadSources, sha, read } = require('./sources.cjs') + +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function') +const suffix = 'retained-output-tail' + +async function heap() { + ;/reset/.test('reset') + for (let i = 0; i < 4; i++) { + await new Promise((resolve) => setImmediate(resolve)) + global.gc() + } + return process.memoryUsage().heapUsed +} + +function emitChunk(stream, chars, index, complete, truncated = false) { + if (truncated) { + stream.write(`${index.toString().padStart(4, '0')}${'x'.repeat(chars - 5)}\n`) + return + } + const label = `${index.toString().padStart(4, '0')}:${suffix}` + const final = `\n${label}${complete ? '\n' : ''}` + const text = `${' '.repeat(chars - final.length)}${final}` + stream.write(text) +} + +async function tail(api, variant, chars, count) { + const streams = [] + const start = await heap() + for (let i = 0; i < count; i++) { + const stream = new PassThrough() + api.pipePluginWorkerOutput(stream, 'info', () => {}) + emitChunk(stream, chars, i, false) + streams.push(stream) + } + const heldDelta = (await heap()) - start + const retains = variant === 'before' + assert.ok( + retains ? heldDelta > chars * count * 0.75 : heldDelta < 768 * 1024, + JSON.stringify({ variant, kind: 'tail', chars, count, heldDelta }) + ) + for (const stream of streams) { + const ended = once(stream, 'end') + stream.end() + await ended + } + const endedDelta = (await heap()) - start + assert.ok(endedDelta < 768 * 1024, JSON.stringify({ variant, endedDelta })) + return { kind: 'tail', variant, chars, count, heldDelta, endedDelta } +} + +function verifyRing(log, count, truncated) { + assert.equal(log.get('plugin').length, Math.min(count, 200)) + for (const [index, row] of log.get('plugin').entries()) { + const inputIndex = index + Math.max(0, count - 200) + assert.equal(row.level, 'info') + assert.equal( + row.line, + truncated + ? `${inputIndex.toString().padStart(4, '0')}${'x'.repeat(8192 - 4 - '… [truncated]'.length)}… [truncated]` + : `${inputIndex.toString().padStart(4, '0')}:${suffix}` + ) + } +} + +async function ring(api, variant, chars, count, truncated = false) { + const log = new api.PluginLogBuffer() + const stream = new PassThrough() + api.pipePluginWorkerOutput(stream, 'info', (level, line) => log.append('plugin', level, line)) + const start = await heap() + for (let i = 0; i < count; i++) { + emitChunk(stream, chars, i, true, truncated) + } + assert.equal(log.get('plugin').length, Math.min(count, 200)) + const heldDelta = (await heap()) - start + const retains = variant === 'before' || variant === 'tail-only' + const expectedParents = Math.min(count, 200) + const fixedBudget = expectedParents * (truncated ? 20 * 1024 : 0) + 768 * 1024 + assert.ok( + retains ? heldDelta > chars * expectedParents * 0.75 : heldDelta < fixedBudget, + JSON.stringify({ variant, kind: 'ring', chars, count, truncated, heldDelta }) + ) + const ended = once(stream, 'end') + stream.end() + await ended + const endedDelta = (await heap()) - start + assert.ok(retains ? endedDelta > chars * expectedParents * 0.75 : endedDelta < fixedBudget) + for (let i = 0; i < 200; i++) { + log.append('plugin', 'info', 'replacement') + } + const evictedDelta = (await heap()) - start + assert.ok(evictedDelta < 768 * 1024, JSON.stringify({ variant, evictedDelta })) + assert.equal(log.get('plugin').length, 200) + return { + kind: 'ring', + variant, + chars, + count, + truncated, + expectedParents, + heldDelta, + endedDelta, + evictedDelta + } +} + +async function behavior(api) { + const lines = [] + const stream = new PassThrough() + api.pipePluginWorkerOutput(stream, 'error', (level, line) => lines.push([level, line])) + for (const chunk of [' \nhello', ' world\n', 'x'.repeat(8193), 'discarded', '\nok\n', suffix]) { + stream.write(chunk) + } + const ended = once(stream, 'end') + stream.end() + await ended + assert.equal(lines.length, 4) + assert.deepEqual(lines[0], ['error', 'hello world']) + assert.equal(lines[1][1].length, 8192) + assert.ok(lines[1][1].endsWith('… [truncated]')) + assert.deepEqual(lines[2], ['error', 'ok']) + assert.deepEqual(lines[3], ['error', suffix]) + const unicode = [] + const direct = new EventEmitter() + direct.setEncoding = (encoding) => assert.equal(encoding, 'utf8') + api.pipePluginWorkerOutput(null, 'info', () => assert.fail('Null stream emitted')) + api.pipePluginWorkerOutput(direct, 'info', (level, line) => unicode.push([level, line])) + for (const chunk of [ + '', + ' \r\n', + 'short\n', + 'twelve chars\n', + '\ud800a\udfff\u0000\u6f22\n', + '😀'.repeat(4096), + '\n', + `${'a'.repeat(8191)}\ud800`, + '\udfff\n', + 'q'.repeat(8193), + 'still discarding', + '\nnext\r\n', + 'unterminated 😀' + ]) { + direct.emit('data', chunk) + } + direct.emit('end') + assert.equal(unicode.length, 8) + assert.deepEqual(unicode[2], ['info', '\ud800a\udfff\u0000\u6f22']) + assert.deepEqual(unicode[3], ['info', '😀'.repeat(4096)]) + assert.equal(unicode[4][1].length, 8192) + assert.ok(unicode[4][1].endsWith('… [truncated]')) + assert.deepEqual(unicode[6], ['info', 'next\r']) + assert.deepEqual(unicode[7], ['info', 'unterminated 😀']) + // Keep value comparisons outside heap controls: they can flatten cons strings. + for (const truncated of [false, true]) { + const log = new api.PluginLogBuffer() + const ringStream = new PassThrough() + api.pipePluginWorkerOutput(ringStream, 'info', (level, line) => + log.append('plugin', level, line) + ) + for (let i = 0; i < 3; i++) { + emitChunk(ringStream, 16 * 1024, i, true, truncated) + } + verifyRing(log, 3, truncated) + const ringEnded = once(ringStream, 'end') + ringStream.end() + await ringEnded + } + return { lines, unicode } +} + +async function main() { + const reports = [], + bundles = {}, + behaviors = {} + for (const variant of ['before', 'tail-only', 'fixed-buffer', 'fixed-fallback']) { + const api = await load(variant) + bundles[variant] = api.provenance + if (variant !== 'before') { + api.resetOwnRetainedStringCopier() + const originalBuffer = globalThis.Buffer + try { + if (variant === 'fixed-fallback') { + globalThis.Buffer = undefined + } + assert.equal(api.ownRetainedString(suffix), suffix) + } finally { + globalThis.Buffer = originalBuffer + } + } + behaviors[variant] = await behavior(api) + reports.push(await tail(api, variant, 64 * 1024, 10)) + reports.push(await tail(api, variant, 4 * 1024 * 1024, 8)) + reports.push(await ring(api, variant, 64 * 1024, 205)) + reports.push(await ring(api, variant, 4 * 1024 * 1024, 8)) + reports.push(await ring(api, variant, 64 * 1024, 205, true)) + reports.push(await ring(api, variant, 4 * 1024 * 1024, 8, true)) + } + assert.deepEqual(behaviors.before, behaviors['tail-only']) + assert.deepEqual(behaviors.before, behaviors['fixed-buffer']) + assert.deepEqual(behaviors.before, behaviors['fixed-fallback']) + const normalSources = loadSources() + let crlfReads = 0 + const crlfSources = loadSources((file) => { + crlfReads += 1 + return read(file).replaceAll('\n', '\r\n') + }) + assert.deepEqual(crlfSources, normalSources) + const args = process.argv.slice(2) + assert.ok(args.length === 0 || (args.length === 2 && args[0] === '--output')) + const output = + args.length === 2 + ? path.resolve(args[1]) + : path.join(__dirname, `${process.versions.electron ? 'electron' : 'node'}-results.json`) + const artifactHashes = Object.fromEntries( + ['reproduce.cjs', 'sources.cjs', 'source-versions.json', 'fix.patch'].map((file) => [ + file, + sha(read(path.join(__dirname, file))) + ]) + ) + fs.writeFileSync( + output, + `${JSON.stringify( + { + runtime: process.versions, + artifactHashes, + crlfLoaderControl: { reads: crlfReads, identical: true }, + bundles, + behaviors, + reports + }, + null, + 2 + )}\n` + ) + console.log(JSON.stringify({ output, passed: reports.length })) +} +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) +setTimeout(() => { + console.error('deadline') + process.exit(2) +}, 30000).unref() diff --git a/docs/audits/plugin-worker-output-retention/source-versions.json b/docs/audits/plugin-worker-output-retention/source-versions.json new file mode 100644 index 00000000000..424395b20c5 --- /dev/null +++ b/docs/audits/plugin-worker-output-retention/source-versions.json @@ -0,0 +1,89 @@ +{ + "publicationTopic": "np-oom-scan-retained-text-slices", + "baselineSha256": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914", + "fixedSha256": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b", + "tailOnlySha256": "5d68af2f66e1466b5a642fa9bb301b8f04ccd2c2741dd5731faac7a2f30c5be5", + "dependencies": { + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "namedRevisions": { + "HEAD": { + "revision": "2e83de3154c4ee1bbeea816734b892c34500a5cc", + "sources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + } + }, + "main": { + "revision": "f78483ec29891ab11f49bb25e6cd628837b1242e", + "sources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + } + }, + "np-oom-scan-retained-text-slices": { + "revision": "0d2efbc0d3b4f902b9bc02f5451c6ff4291405cf", + "sources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + } + }, + "291b4ddd6f1c1af480169885e0fda7f9c78ff053": { + "revision": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "sources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + } + }, + "e0826956fcfc532f5a1e55b5e081f2e57e553c43": { + "revision": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sources": { + "src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914", + "src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8", + "src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b", + "src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70", + "src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e", + "src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420", + "src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689", + "src/shared/own-retained-string.ts": null, + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + } + } + }, + "historicalScope": "v1.4.198 has the identical parser, ring and caller modules but lacks own-retained-string.ts. The baseline bundle imports only the unchanged parser/ring. Fixed and tail-only variants use the recorded publication helper; this is not a historical application binary." +} diff --git a/docs/audits/plugin-worker-output-retention/sources.cjs b/docs/audits/plugin-worker-output-retention/sources.cjs new file mode 100644 index 00000000000..54005ea1a69 --- /dev/null +++ b/docs/audits/plugin-worker-output-retention/sources.cjs @@ -0,0 +1,94 @@ +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 sourcePath = 'src/main/plugins/plugin-worker-output-buffer.ts' +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') + +function loadSources(readText = read) { + const versionsText = read(path.join(__dirname, 'source-versions.json')) + const versions = JSON.parse(versionsText) + const patches = parsePatch(canonicalLf(readText(path.join(__dirname, 'fix.patch')))) + assert.equal(patches.length, 1) + assert.equal(patches[0].newFileName, `b/${sourcePath}`) + const current = canonicalLf(readText(path.join(root, sourcePath))) + const before = applyPatch(current, reversePatch(patches[0])) + assert.notEqual(before, false, 'The parser no longer matches the reviewed patch') + assert.equal(sha(before), versions.baselineSha256) + assert.equal(sha(current), versions.fixedSha256) + const checkedSources = { [sourcePath]: sha(current) } + for (const [relative, expected] of Object.entries(versions.dependencies)) { + const actual = sha(canonicalLf(readText(path.join(root, relative)))) + assert.equal(actual, expected, `Reviewed dependency changed: ${relative}`) + checkedSources[relative] = actual + } + return { before, current, checkedSources, versions, versionsSha256: sha(versionsText) } +} + +async function load(variant) { + const checked = loadSources() + let source = variant === 'before' ? checked.before : checked.current + if (variant === 'tail-only') { + source = `import { ownRetainedString } from '../../shared/own-retained-string'\n${checked.before}` + assert.equal(source.split(' buffered += segment').length, 2) + source = source.replace( + ' buffered += segment', + ' buffered += newline === -1 ? ownRetainedString(segment) : segment' + ) + assert.equal(sha(source), checked.versions.tailOnlySha256) + } + const entries = [ + "export { pipePluginWorkerOutput } from './src/main/plugins/plugin-worker-output-buffer'", + "export { PluginLogBuffer } from './src/main/plugins/plugin-log-buffer'" + ] + if (variant !== 'before') { + entries.push( + "export { ownRetainedString, resetOwnRetainedStringCopier } from './src/shared/own-retained-string'" + ) + } + const evaluatedSources = {} + const built = await build({ + stdin: { contents: entries.join('\n'), resolveDir: root }, + platform: 'node', + format: 'cjs', + bundle: true, + write: false, + plugins: [ + { + name: 'hash-fenced-plugin-output', + setup(builder) { + builder.onLoad({ filter: /\.ts$/ }, ({ path: file }) => { + const relative = path.relative(root, file).split(path.sep).join('/') + assert.ok(Object.hasOwn(checked.checkedSources, relative), relative) + const contents = relative === sourcePath ? source : read(file) + evaluatedSources[relative] = sha(contents) + return { contents, loader: 'ts' } + }) + } + } + ] + }) + const filename = path.join(__dirname, `${variant}-bundle.cjs`) + const loaded = new Module(filename, module) + loaded.filename = filename + loaded.paths = Module._nodeModulePaths(root) + loaded._compile(built.outputFiles[0].text, filename) + return { + ...loaded.exports, + provenance: { + checkedSources: checked.checkedSources, + evaluatedSources, + sourceVersionsSha256: checked.versionsSha256, + bundleSha256: sha(built.outputFiles[0].text) + } + } +} + +module.exports = { load, loadSources, sha, read } diff --git a/docs/audits/pty-detector-retention/README.md b/docs/audits/pty-detector-retention/README.md new file mode 100644 index 00000000000..21637ed610a --- /dev/null +++ b/docs/audits/pty-detector-retention/README.md @@ -0,0 +1,59 @@ +# Retained PTY detector input + +The advertised-URL watcher keeps a 4,096-character carry for each bound PTY and +16,384 characters for each of at most 32 unbound PTYs. The Command Code status +detector keeps 300 characters before its agent-specific prefilter, including for +ordinary shell, Claude, and Codex output. Each could keep the whole original +input alive through a V8 sliced string. + +The fix uses the existing `ownRetainedString` copier when dropping oversized +input. It preserves URL reconstruction, status detection, UTF-16 code units, +binding/unbinding, cache limits, and remote/local authority. These are three +additional boundaries in [#20960](https://github.com/stablyai/orca/pull/20960). + +## Reproduce + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc docs/audits/pty-detector-retention/reproduce.mjs +``` + +The script bundles the actual detector and watcher. The baseline removes only +the three copy calls in memory. Each input has its own live owner; URL cases +use separate watcher instances to isolate each carry. Heap is measured after +GC, before completing the partial URLs and verifying cleanup. Results include +owner overhead, not just text. [Bundle hashes and measurements](./results.json). + +| Case | Input per owner | Owners | Heap before | Heap after | +| ------------------- | ---------------: | -----: | ----------: | ---------: | +| Status detector | 64 Ki characters | 32 | 2,112,688 | 41,400 | +| Bound URL carry | 64 Ki characters | 32 | 2,173,848 | 204,112 | +| URL pending binding | 64 Ki characters | 32 | 2,148,824 | 575,512 | +| Status detector | 4 Mi characters | 8 | 33,557,144 | 6,504 | +| Bound URL carry | 4 Mi characters | 8 | 33,569,168 | 47,112 | +| URL pending binding | 4 Mi characters | 8 | 33,568,936 | 144,504 | + +Captured with Node v26.6.0 on macOS. Three GC regression tests failed before the +fix, retaining about 32 MiB each, and pass afterward. The five-suite run passes +164 tests including existing URL/status behavior and copier Unicode/fallback +tests. + +## Scope and limits + +Main feeds both observers before renderer batching. Default daemon bulk output +frames are at most 64 Ki UTF-16 characters; main's later 16 Ki-character batching +does not bound these readers. The 4 Mi-character cases demonstrate the retaining +mechanism under larger inputs, not normal daemon frame size. Both implementations +also exist in `v1.4.198`. + +Transformed frames bypass ordinary chunk slicing but still face the daemon's +16 MiB encoded-line limit. Native fallback output has no application chunk cap; +this audit does not establish multi-MiB native reads. Ordinary relay chunks are +16 Ki characters. The actual main feed is `orca-runtime-on-pty-data.ts` and the +ordinary daemon bound is in `daemon-stream-data-batcher.ts`. + +These are per-owner last-input costs, not unbounded growth for a fixed set of +PTYs and fixed-size frames. Owners consuming the same input can share the same +backing string, so the three measurements must not be added as independent +process costs. Unbind removes URL buffers and pending entries. This improves +memory proportional to active owners; it does not establish the cause or growth +rate of #19831 or #19768. Copy work is bounded by the small retained tails. diff --git a/docs/audits/pty-detector-retention/reproduce.mjs b/docs/audits/pty-detector-retention/reproduce.mjs new file mode 100644 index 00000000000..b6c2f3a8c9e --- /dev/null +++ b/docs/audits/pty-detector-retention/reproduce.mjs @@ -0,0 +1,144 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { build } from 'esbuild' + +if (process.env.ORCA_BACKGROUND_LAUNCH !== '1' || typeof global.gc !== 'function') { + throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1 node --expose-gc') +} +const root = fileURLToPath(new URL('../../../', import.meta.url)) +const replacements = { + 'command-code-output-status.ts': [ + 'ownRetainedString(data.slice(-RECENT_TEXT_LIMIT))', + 'data.slice(-RECENT_TEXT_LIMIT)' + ], + 'advertised-url-parsing.ts': [ + 'ownRetainedString(chunk.slice(-PER_PTY_BUFFER_LIMIT))', + 'chunk.slice(-PER_PTY_BUFFER_LIMIT)' + ], + 'advertised-url-watcher.ts': [ + 'ownRetainedString(combined.slice(-PENDING_PRE_BIND_LIMIT))', + 'combined.slice(-PENDING_PRE_BIND_LIMIT)' + ] +} +const results = [] +const bundles = {} + +function heapAfterGc() { + global.gc() + global.gc() + return process.memoryUsage().heapUsed +} + +function measure(makeOwner, validate, inputChars, count) { + const before = heapAfterGc() + const owners = Array.from({ length: count }, (_, index) => { + const prefix = `${index}:` + const suffix = `\nhttp://localhost:${4100 + index}` + return makeOwner( + `${prefix}${'x'.repeat(inputChars - prefix.length - suffix.length)}${suffix}`, + index + ) + }) + const heapDelta = heapAfterGc() - before + owners.forEach(validate) + return { inputChars, count, heapDelta } +} + +for (const fixed of [false, true]) { + const result = await build({ + stdin: { + contents: ` + export { createCommandCodeOutputStatusDetector } from './src/shared/command-code-output-status' + export { AdvertisedUrlWatcher } from './src/main/ports/advertised-url-watcher' + `, + resolveDir: root, + loader: 'ts' + }, + bundle: true, + platform: 'node', + format: 'esm', + write: false, + plugins: fixed + ? [] + : [ + { + name: 'baseline-without-detector-tail-copy', + setup(builder) { + builder.onLoad( + { + filter: + /(?:command-code-output-status|advertised-url-parsing|advertised-url-watcher)\.ts$/ + }, + async ({ path }) => { + const source = await readFile(path, 'utf8') + const replacement = Object.entries(replacements).find(([name]) => + path.endsWith(name) + )?.[1] + if (!replacement || !source.includes(replacement[0])) { + throw new Error('The copy boundary changed; update the baseline transform') + } + return { contents: source.replaceAll(...replacement), loader: 'ts' } + } + ) + } + } + ] + }) + const bundle = result.outputFiles[0].text + bundles[fixed ? 'after' : 'before'] = createHash('sha256').update(bundle).digest('hex') + const { createCommandCodeOutputStatusDetector, AdvertisedUrlWatcher } = await import( + `data:text/javascript;base64,${Buffer.from(bundle).toString('base64')}` + ) + for (const [inputChars, count] of [ + [64 * 1024, 32], + [4 * 1024 * 1024, 8] + ]) { + results.push({ + kind: 'command-code-detector', + fixed, + ...measure( + (data) => { + const detector = createCommandCodeOutputStatusDetector({ onWorking: () => {} }) + detector.observe(data) + return detector + }, + (detector) => assert.equal(detector.observe('\nordinary output\n'), false), + inputChars, + count + ) + }) + for (const bound of [true, false]) { + results.push({ + kind: bound ? 'url-bound-pty' : 'url-before-binding', + fixed, + ...measure( + (data) => { + const watcher = new AdvertisedUrlWatcher() + if (bound) { + watcher.bindPty('pty', 'workspace') + } + watcher.ingest('pty', data) + return watcher + }, + (watcher, index) => { + watcher.bindPty('pty', 'workspace') + watcher.ingest('pty', '/\n') + assert.equal( + watcher.lookup('workspace', 4100 + index)?.origin, + `http://localhost:${4100 + index}` + ) + watcher.unbindPty('pty') + assert.equal(watcher.lookup('workspace', 4100 + index), undefined) + }, + inputChars, + count + ) + }) + } + } +} +console.log( + JSON.stringify({ node: process.version, platform: process.platform, bundles, results }, null, 2) +) diff --git a/docs/audits/pty-detector-retention/results.json b/docs/audits/pty-detector-retention/results.json new file mode 100644 index 00000000000..5bfdc48c4e8 --- /dev/null +++ b/docs/audits/pty-detector-retention/results.json @@ -0,0 +1,94 @@ +{ + "node": "v26.6.0", + "platform": "darwin", + "bundles": { + "before": "139646a3dc9316a104f472c86e674be062da8fbdf5a35563624557d44d27f471", + "after": "a2efe55354954f78e187fd65a247b51d97017776892979b631725ff686065168" + }, + "results": [ + { + "kind": "command-code-detector", + "fixed": false, + "inputChars": 65536, + "count": 32, + "heapDelta": 2112688 + }, + { + "kind": "url-bound-pty", + "fixed": false, + "inputChars": 65536, + "count": 32, + "heapDelta": 2173848 + }, + { + "kind": "url-before-binding", + "fixed": false, + "inputChars": 65536, + "count": 32, + "heapDelta": 2148824 + }, + { + "kind": "command-code-detector", + "fixed": false, + "inputChars": 4194304, + "count": 8, + "heapDelta": 33557144 + }, + { + "kind": "url-bound-pty", + "fixed": false, + "inputChars": 4194304, + "count": 8, + "heapDelta": 33569168 + }, + { + "kind": "url-before-binding", + "fixed": false, + "inputChars": 4194304, + "count": 8, + "heapDelta": 33568936 + }, + { + "kind": "command-code-detector", + "fixed": true, + "inputChars": 65536, + "count": 32, + "heapDelta": 41400 + }, + { + "kind": "url-bound-pty", + "fixed": true, + "inputChars": 65536, + "count": 32, + "heapDelta": 204112 + }, + { + "kind": "url-before-binding", + "fixed": true, + "inputChars": 65536, + "count": 32, + "heapDelta": 575512 + }, + { + "kind": "command-code-detector", + "fixed": true, + "inputChars": 4194304, + "count": 8, + "heapDelta": 6504 + }, + { + "kind": "url-bound-pty", + "fixed": true, + "inputChars": 4194304, + "count": 8, + "heapDelta": 47112 + }, + { + "kind": "url-before-binding", + "fixed": true, + "inputChars": 4194304, + "count": 8, + "heapDelta": 144504 + } + ] +} diff --git a/docs/audits/retained-text-slices/README.md b/docs/audits/retained-text-slices/README.md new file mode 100644 index 00000000000..15bc2b39145 --- /dev/null +++ b/docs/audits/retained-text-slices/README.md @@ -0,0 +1,75 @@ +# Retained CI and terminal text tails + +Capped V8 string slices can keep their entire original input alive. The affected +CI excerpt cache accepts 128 entries of 16 KiB text, from downloads up to 64 MiB. +GitLab's raw-trace clamp reaches the same shared excerpt function. Terminal +session/eager/shutdown buffers, deferred reattach queues, recent-output buffers, +and error surfaces also retained oversized parents despite their logical caps. + +The fix reuses the existing shared `ownRetainedString` copier for CI, persisted +session tails, and main/relay recent output. Renderer queues and errors reuse +their existing `flattenRetainedSlice` helper. Content, Unicode, earlier-error +selection, cache counts, and transport payloads stay identical. Ordinary +untruncated terminal chunks keep their existing path. Main/relay recent output +preserves chunk boundaries for path-candidate backfill. + +Local persisted scrollback is already pruned; the session-buffer fix primarily +covers remote or not-yet-classified owners. Queue and error fixes cover local +and remote output. The main/relay recent-output buffer has a configurable cap, +64 Ki characters by default. + +## Reproduce + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc docs/audits/retained-text-slices/reproduce.mjs +``` + +The script bundles actual production functions. Its baseline removes only the +six new copy boundaries in memory; production files are not changed. Each case +retains eight distinct inputs: 2 Mi characters per CI log and 4 Mi characters +per terminal input. It measures heap after GC and clears V8's independent legacy +RegExp input reference. Bundle hashes and measurements are in +[results.json](./results.json). + +| Case | Returned bytes, all eight | Retained heap before | After | +| -------------------------------- | ------------------------: | -------------------: | --------: | +| GitHub long line | 131,072 | 16,787,512 | 145,224 | +| GitHub earlier Unicode error | 131,064 | 33,577,840 | 112,744 | +| GitLab long line | 131,072 | 16,793,640 | 147,312 | +| Persisted terminal buffers | 4,194,304 | 33,555,624 | 4,195,032 | +| Eager/pre-handler/shutdown tails | 4,194,304 | 33,556,040 | 4,202,608 | +| Main/relay recent output | 524,288 | 33,558,752 | 527,384 | +| Terminal error surfaces | 32,000 | 33,555,864 | 33,336 | +| Deferred reattach tails | 4,194,304 | 33,565,384 | 4,198,352 | + +Captured on macOS with Node v26.6.0. Heap samples include allocator/GC variation; +the large separation is the relevant result. Regression tests also retain the +actual error state and shutdown/reattach/recent-output queue objects. + +Validation passed: 41 tests across six CI/provider/helper suites; 69 tests across +six terminal storage/ownership/UTF-8 suites; 28 tests across three error/reattach +suites; and a final 63 tests across seven recent-output/CI/terminal/copier suites. +These are per-run counts and overlap. Full typecheck and changed-code quality pass. + +All six cap/slice paths exist in `v1.4.198`. Neither #19831 nor #19768 establishes +the CI-log viewing or oversized terminal inputs required for incident attribution. +Copying costs scale with retained caps: 16 KiB per CI excerpt, 4,000 characters +per error, 512 KiB for the largest byte-capped buffer, and 512 Ki characters for +deferred reattach. The change does not reduce temporary original-input allocation. + +The follow-up [PTY detector reproduction](../pty-detector-retention/README.md) +adds three boundaries in the same PR: advertised-URL carries, output waiting for +workspace binding, and Command Code status carries used by ordinary PTYs too. +Thirty-two production-sized 64 Ki-character inputs retain about 2.1 MB in each +isolated baseline. Owned carries reduce that to about 41 KB, 204 KB, or 575 KB, +including the different owner objects. These per-owner costs are not an +unbounded growth curve, and readers of the same input can share its parent. +The follow-up adds 164 passing tests across five detector/URL/copier suites. + +The [Claude task metadata reproduction](../claude-task-retention/README.md) adds +the shared 512-character description/name boundary. JSON-parsed task frames +retained their parents in the actual live, settled, and recently removed tracker +entries. Eight 4 Mi-character inputs retained about 32 MiB before the copy and +7–12 KB afterward; 32 smaller 64 Ki-character inputs retained about 2.1 MB before +and 25–45 KB afterward. These synthetic fields establish a retaining mechanism, +not the trigger of a reported incident. diff --git a/docs/audits/retained-text-slices/reproduce.mjs b/docs/audits/retained-text-slices/reproduce.mjs new file mode 100644 index 00000000000..86b5e818e8b --- /dev/null +++ b/docs/audits/retained-text-slices/reproduce.mjs @@ -0,0 +1,161 @@ +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { build } from 'esbuild' + +if (process.env.ORCA_BACKGROUND_LAUNCH !== '1' || typeof global.gc !== 'function') { + throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1 node --expose-gc') +} +const root = fileURLToPath(new URL('../../../', import.meta.url)) +const replacements = { + 'recent-pty-output-buffer.ts': [ + 'this.chunks = [data.length > this.limit ? ownRetainedString(data.slice(-this.limit)) : data]', + 'this.chunks = [data.slice(-this.limit)]' + ], + 'check-job-log-tail-slice.ts': [ + 'return ownRetainedString(buildCheckLogTail(logText))', + 'return buildCheckLogTail(logText)' + ], + 'workspace-session-terminal-buffers.ts': [ + 'return ownRetainedString(\n clampUtf8TextTail(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT).text\n )', + 'return clampUtf8TextTail(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT).text' + ], + 'pty-eager-buffer-clamp.ts': [ + 'data: tail.text.length < data.length ? flattenRetainedSlice(tail.text) : tail.text', + 'data: tail.text' + ], + 'terminal-error-accumulation.ts': ['return flattenRetainedSlice(bounded)', 'return bounded'], + 'deferred-reattach-live-data-queue.ts': [ + 'flattenRetainedSlice(chunk.data.slice(-MAX_DEFERRED_REATTACH_LIVE_CHARS))', + 'chunk.data.slice(-MAX_DEFERRED_REATTACH_LIVE_CHARS)' + ] +} +const results = [] +const bundles = {} +const parentChars = 2 * 1024 * 1024 +const count = 8 + +function measure(excerpt, makeLog) { + global.gc() + const before = process.memoryUsage().heapUsed + const retained = Array.from({ length: count }, (_, index) => excerpt(makeLog(index))) + // Clear V8's independent legacy RegExp input reference before measuring our retained values. + void /probe/.test('probe') + global.gc() + global.gc() + const heapDelta = process.memoryUsage().heapUsed - before + return { + entries: retained.length, + logicalChars: retained.reduce((total, text) => total + text.length, 0), + logicalBytes: retained.reduce((total, text) => total + Buffer.byteLength(text), 0), + heapDelta + } +} + +for (const fixed of [false, true]) { + const result = await build({ + stdin: { + contents: ` + export { RecentPtyOutputBuffer } from './src/main/runtime/recent-pty-output-buffer' + export { sliceCheckLogTail } from './src/shared/check-job-log-tail-slice' + export { gitLabJobTraceToLogExcerpt } from './src/shared/gitlab-job-log-excerpt' + export { capTerminalScrollbackSessionBuffer } from './src/shared/workspace-session-terminal-buffers' + export { clampUtf8Tail } from './src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp' + export { boundTerminalErrorSurface } from './src/renderer/src/components/terminal-pane/terminal-error-accumulation' + export { DeferredReattachLiveDataQueue } from './src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue' + `, + resolveDir: root, + loader: 'ts' + }, + bundle: true, + platform: 'node', + format: 'esm', + write: false, + plugins: fixed + ? [] + : [ + { + name: 'baseline-without-retained-tail-copy', + setup(builder) { + builder.onLoad( + { + filter: + /(?:recent-pty-output-buffer|check-job-log-tail-slice|workspace-session-terminal-buffers|pty-eager-buffer-clamp|terminal-error-accumulation|deferred-reattach-live-data-queue)\.ts$/ + }, + async ({ path }) => { + const source = await readFile(path, 'utf8') + const replacement = Object.entries(replacements).find(([name]) => + path.endsWith(name) + )?.[1] + if (!replacement || !source.includes(replacement[0])) { + throw new Error('The copy boundary changed; update the baseline transform') + } + return { + contents: source.replaceAll(...replacement), + loader: 'ts' + } + } + ) + } + } + ] + }) + const bundle = result.outputFiles[0].text + bundles[fixed ? 'after' : 'before'] = createHash('sha256').update(bundle).digest('hex') + const { + sliceCheckLogTail, + gitLabJobTraceToLogExcerpt, + capTerminalScrollbackSessionBuffer, + clampUtf8Tail, + boundTerminalErrorSurface, + DeferredReattachLiveDataQueue, + RecentPtyOutputBuffer + } = await import(`data:text/javascript;base64,${Buffer.from(bundle).toString('base64')}`) + for (const [kind, makeLog, excerpt] of [ + ['github-long-line', (i) => `${i}:${'x'.repeat(parentChars)}`, sliceCheckLogTail], + [ + 'github-earlier-error', + (i) => `error: ${i}:${'界'.repeat(parentChars)}\n${'recent\n'.repeat(100)}`, + sliceCheckLogTail + ], + ['gitlab-long-line', (i) => `${i}:${'x'.repeat(parentChars)}`, gitLabJobTraceToLogExcerpt], + [ + 'terminal-session-buffer', + (i) => `${i}:${'x'.repeat(parentChars * 2)}`, + capTerminalScrollbackSessionBuffer + ], + [ + 'terminal-eager-buffer', + (i) => `${i}:${'x'.repeat(parentChars * 2)}`, + (text) => clampUtf8Tail(text, 512 * 1024).data + ], + [ + 'terminal-recent-output', + (i) => `${i}:${'x'.repeat(parentChars * 2)}`, + (data) => { + const buffer = new RecentPtyOutputBuffer() + buffer.append(data) + return buffer.read() + } + ], + ['terminal-error', (i) => `${'x'.repeat(parentChars * 2)}:${i}`, boundTerminalErrorSurface], + [ + 'terminal-deferred-reattach', + (i) => `${i}:${'x'.repeat(parentChars * 2)}`, + (data) => { + const queue = new DeferredReattachLiveDataQueue() + queue.enqueue({ data, ptyId: 'p', streamGeneration: 1 }) + return queue.takeAll()[0].data + } + ] + ]) { + results.push({ kind, fixed, ...measure(excerpt, makeLog) }) + } +} +console.log( + JSON.stringify( + { node: process.version, platform: process.platform, parentChars, count, bundles, results }, + null, + 2 + ) +) diff --git a/docs/audits/retained-text-slices/results.json b/docs/audits/retained-text-slices/results.json new file mode 100644 index 00000000000..b16d4ab323a --- /dev/null +++ b/docs/audits/retained-text-slices/results.json @@ -0,0 +1,140 @@ +{ + "node": "v26.6.0", + "platform": "darwin", + "parentChars": 2097152, + "count": 8, + "bundles": { + "before": "b1f295283888921d8d473649185b5146ed8379bc8344b2bf04a0a4fcf334b5ec", + "after": "74444cedf1564c7092a46058f331ad2acc55b222b09340d5a19ca5a76de611fb" + }, + "results": [ + { + "kind": "github-long-line", + "fixed": false, + "entries": 8, + "logicalChars": 131072, + "logicalBytes": 131072, + "heapDelta": 16787512 + }, + { + "kind": "github-earlier-error", + "fixed": false, + "entries": 8, + "logicalChars": 43816, + "logicalBytes": 131064, + "heapDelta": 33577840 + }, + { + "kind": "gitlab-long-line", + "fixed": false, + "entries": 8, + "logicalChars": 131072, + "logicalBytes": 131072, + "heapDelta": 16793640 + }, + { + "kind": "terminal-session-buffer", + "fixed": false, + "entries": 8, + "logicalChars": 4194304, + "logicalBytes": 4194304, + "heapDelta": 33555624 + }, + { + "kind": "terminal-eager-buffer", + "fixed": false, + "entries": 8, + "logicalChars": 4194304, + "logicalBytes": 4194304, + "heapDelta": 33556040 + }, + { + "kind": "terminal-recent-output", + "fixed": false, + "entries": 8, + "logicalChars": 524288, + "logicalBytes": 524288, + "heapDelta": 33558752 + }, + { + "kind": "terminal-error", + "fixed": false, + "entries": 8, + "logicalChars": 32000, + "logicalBytes": 32000, + "heapDelta": 33555864 + }, + { + "kind": "terminal-deferred-reattach", + "fixed": false, + "entries": 8, + "logicalChars": 4194304, + "logicalBytes": 4194304, + "heapDelta": 33565384 + }, + { + "kind": "github-long-line", + "fixed": true, + "entries": 8, + "logicalChars": 131072, + "logicalBytes": 131072, + "heapDelta": 145224 + }, + { + "kind": "github-earlier-error", + "fixed": true, + "entries": 8, + "logicalChars": 43816, + "logicalBytes": 131064, + "heapDelta": 112744 + }, + { + "kind": "gitlab-long-line", + "fixed": true, + "entries": 8, + "logicalChars": 131072, + "logicalBytes": 131072, + "heapDelta": 147312 + }, + { + "kind": "terminal-session-buffer", + "fixed": true, + "entries": 8, + "logicalChars": 4194304, + "logicalBytes": 4194304, + "heapDelta": 4195032 + }, + { + "kind": "terminal-eager-buffer", + "fixed": true, + "entries": 8, + "logicalChars": 4194304, + "logicalBytes": 4194304, + "heapDelta": 4202608 + }, + { + "kind": "terminal-recent-output", + "fixed": true, + "entries": 8, + "logicalChars": 524288, + "logicalBytes": 524288, + "heapDelta": 527384 + }, + { + "kind": "terminal-error", + "fixed": true, + "entries": 8, + "logicalChars": 32000, + "logicalBytes": 32000, + "heapDelta": 33336 + }, + { + "kind": "terminal-deferred-reattach", + "fixed": true, + "entries": 8, + "logicalChars": 4194304, + "logicalBytes": 4194304, + "heapDelta": 4198352 + } + ] +} diff --git a/docs/audits/terminal-mode-tail-retention/README.md b/docs/audits/terminal-mode-tail-retention/README.md new file mode 100644 index 00000000000..df5635a6f3a --- /dev/null +++ b/docs/audits/terminal-mode-tail-retention/README.md @@ -0,0 +1,122 @@ +# Retained terminal mode scan tails + +The kitty keyboard tracker and daemon mouse-mode mirror retain an incomplete +escape-sequence tail of at most 4,096 UTF-16 code units. A V8 sliced string can +keep the entire consumed input alive through that small tail. An ordinary +split grouped mode sequence, `ESC[?1049;2004;1000;`, is enough: its 18-character +tail retains each input backing string while its parser stays idle. + +The correction copies only accepted incomplete tails through the existing +`ownRetainedString` helper. Empty/rejected tails and short ESC/CSI prefixes keep +their existing behavior; the helper leaves strings shorter than 13 code units +alone. Parser state, live/replay semantics, stack caps, mode flags, and wire +content are unchanged. These are additional boundaries in +[#20960](https://github.com/stablyai/orca/pull/20960), alongside the +[PTY detector carries](../pty-detector-retention/README.md). + +## Reproduce + +```sh +ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/terminal-mode-tail-retention/reproduce.cjs +``` + +Run the same script with the installed Electron executable, setting +`ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`, and passing the same +Node flags. This launches no app window or native PTY. Each run has a 30-second +deadline and writes either [Node results](./node-results.json) or +[Electron results](./electron-results.json). + +The loader reads the actual five source modules, verifies their fixed hashes, +reverses only the three copy calls/imports in memory for the baseline, and +verifies the resulting baseline hashes. All evaluated module and bundle hashes +are recorded. It needs no Git history, absolute development paths, or ignored +notes. CRLF source text is normalized before hashing. The parsers and flag +parser match `v1.4.198`; all five modules match the pre-extension topic +`8d599520e44654a5c28e9930e3070c00d6499931`, except for these copy calls. This +tests current dependencies and the selected source modules, not a historical +application binary. See [source versions](./source-versions.json). + +Each runtime checks 42 bounded heap cases: baseline, fixed Buffer copier, and +fixed Bufferless copier; kitty live/replay, mouse live; 32 distinct 64-Ki-character +inputs and eight 4-Mi-character inputs; short, complete, oversized, and C1-CSI +tail controls. Completion must reconstruct the correct modes and release the +large backing strings. Additional controls preserve replay push idempotence, +the 16-frame live stack cap, alternate-screen state, snapshot unknownness, +mouse encodings, and RIS with a trailing partial sequence. + +Both runtimes pass all 42 cases. Representative live-path heap deltas in bytes: + +| Runtime | Parser | Input × owners | Baseline | Buffer copy | Bufferless copy | +| --------------------- | ------ | -------------- | ---------: | ----------: | --------------: | +| Node 26 | Kitty | 64 Ki × 32 | 2,120,952 | 18,376 | 9,480 | +| Node 26 | Mouse | 64 Ki × 32 | 2,111,984 | 15,112 | 9,336 | +| Node 26 | Kitty | 4 Mi × 8 | 33,557,944 | 3,448 | 3,448 | +| Node 26 | Mouse | 4 Mi × 8 | 33,557,072 | 1,312 | 1,312 | +| Electron 43 / Node 24 | Kitty | 64 Ki × 32 | 2,111,864 | 12,244 | 5,192 | +| Electron 43 / Node 24 | Mouse | 64 Ki × 32 | 2,103,444 | 14,432 | 8,004 | +| Electron 43 / Node 24 | Kitty | 4 Mi × 8 | 33,556,316 | 1,884 | 2,604 | +| Electron 43 / Node 24 | Mouse | 4 Mi × 8 | 33,556,172 | 776 | 752 | + +Heap readings include owner overhead and follow forced GC. The harness clears +V8's last successful regexp input identically in baseline and fixed cases to +isolate per-owner storage. That independent process-wide regexp reference can +keep a most-recent input alive until another successful match; this change does +not eliminate it. The Bufferless selection is memoized while Buffer is absent, +then Buffer is restored before measuring; it exercises the actual renderer +fallback without running a browser renderer. + +Two permanent kitty heap regressions fail before the correction at 33,560,832 +and 33,575,040 retained bytes against a 2-MiB limit. They also verify that the +retained prefix completes correctly and that replay/pop/snapshot state remains +valid. Existing parser and copier tests provide the wider protocol controls. +The two mouse regressions likewise fail before the correction at 33,559,240 and +33,573,200 bytes, and pass afterward with both CSI encodings. The five-suite +run passes 98 tests, including actual headless-emulator mode snapshots; Node +and renderer TypeScript checks pass. + +## Callers and lifetime + +- Kitty renderer panes create or reuse one tracker per pane in + `connect-pane-pty.ts:160`. `write-pty-output-to-xterm.ts:23` feeds application + output; `apply-reattach-payload.ts` and `hidden-output-seq-and-skip.ts` feed + replay. Fresh spawn and exit reset it, and + `terminal-pane-pane-closed.ts:69` deletes the map entry. Dashboard previews + own another tracker per effect (`AgentTerminalPreview.tsx:116`); cleanup + removes its listeners and disposes its terminal. +- Main's `orca-runtime-capture-provider-terminal-buffer.ts:23` registers + temporary live scanners during provider snapshot acquisition and removes + them in `finally`. It creates a persistent tracker only after observing an + alternate-screen transition (`:48–57`). `orca-runtime-on-pty-data.ts:30` + feeds those trackers before later output processing. Exit, floating PTY + liveness cleanup, and provider generation reset delete the persistent entry. +- The daemon does not directly instantiate the kitty tracker, despite its + old class comment: its kitty flags come from xterm. No mobile bundle imports + this class. Mobile can exercise main-side snapshot acquisition; SSH output + can reach main and renderer trackers through the existing provider routes. +- Mouse mirrors are owned by `HeadlessEmulator` (`headless-emulator.ts:59`). + Async writes scan after xterm parses the data (`:190`); synchronous live and + cold-restore writes scan at `:224`. Both daemon sessions and main's headless + projections use this emulator. It therefore also covers local/remote host + emulators serving mobile clients. Emulator disposal stops future writes; + eventual owner release removes the mirror. Completing/replacing its tail + also releases the old backing string. No ownership or shutdown rule changes. + +## Scope and limits + +This is a per-owner last-input cost. It does not grow indefinitely with a fixed +set of parsers and bounded input chunks, and further output often completes or +replaces the tail. Multiple readers of the same input can share its backing +storage; do not sum their measurements as independent process memory. + +Ordinary daemon bulk frames delivered to main are at most 64 Ki characters +(`daemon-stream-data-batcher.ts:35`), and ordinary relay output slices are +16 Ki characters (`relay/pty-handler.ts:343`). Mouse scanning inside the daemon +happens before outgoing stream framing. The 64-Ki cases demonstrate the issue +at a normal main-input bound; the 4-Mi cases amplify the mechanism, not a claim +that ordinary native reads or daemon frames have that size. Replay inputs and +transformed streams follow their own existing limits. No network, application +renderer, operating-system PTY, or incident heap was used in this proof. + +This reduces retained output in local and SSH paths without changing published +terminal content. It neither establishes the trigger in #19831/#19768 nor +explains a reported sustained growth rate or multi-gigabyte incident by itself. diff --git a/docs/audits/terminal-mode-tail-retention/electron-results.json b/docs/audits/terminal-mode-tail-retention/electron-results.json new file mode 100644 index 00000000000..e81d9abc27c --- /dev/null +++ b/docs/audits/terminal-mode-tail-retention/electron-results.json @@ -0,0 +1,467 @@ +{ + "node": "v24.21.0", + "electron": "43.7.0", + "v8": "15.0.245.31-electron.0", + "platform": "darwin", + "runnerSha256": "b78dc54a08345732235d4fcc1f72ca1534b00f89280981beda00667d22b8f291", + "loaderSha256": "730da9091abba3997432657aef4a2e6e7a3dd0e9218ea78f1838806b6c0fa789", + "pendingTailCodeUnits": 18, + "clearedRegexStatics": true, + "bundles": { + "baseline": { + "bundleSha256": "6023f5d6868f5db601f6883acda5d56990d204084a6a3c812799aff1e533a5b0", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-kitty-keyboard-mode-tracker.ts": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225" + }, + "sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a" + }, + "fixed-buffer": { + "bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225" + }, + "sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a" + }, + "fixed-fallback": { + "bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225" + }, + "sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a" + } + }, + "reports": [ + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 2111864, + "afterCompletionDelta": 29832 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 33556316, + "afterCompletionDelta": 9244 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 2102416, + "afterCompletionDelta": 4916 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 33556304, + "afterCompletionDelta": 1968 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 1756, + "afterCompletionDelta": 1712 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 1652, + "afterCompletionDelta": 2880 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 1652, + "afterCompletionDelta": 3024 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 33556316, + "afterCompletionDelta": 1692 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 2103444, + "afterCompletionDelta": 5584 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 33556172, + "afterCompletionDelta": 1496 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 672, + "afterCompletionDelta": 644 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 520, + "afterCompletionDelta": 532 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 520, + "afterCompletionDelta": 532 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 33555224, + "afterCompletionDelta": 560 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 12244, + "afterCompletionDelta": 11632 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 1884, + "afterCompletionDelta": 1640 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 5148, + "afterCompletionDelta": 4176 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 1884, + "afterCompletionDelta": 1640 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 1756, + "afterCompletionDelta": 1688 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 1652, + "afterCompletionDelta": 2808 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 1628, + "afterCompletionDelta": 1640 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 1884, + "afterCompletionDelta": 1652 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 14432, + "afterCompletionDelta": 13692 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 776, + "afterCompletionDelta": 532 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 672, + "afterCompletionDelta": 628 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 520, + "afterCompletionDelta": 532 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 520, + "afterCompletionDelta": 532 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 792, + "afterCompletionDelta": 560 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 5192, + "afterCompletionDelta": 4180 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 2604, + "afterCompletionDelta": 2360 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 5176, + "afterCompletionDelta": 7104 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 1884, + "afterCompletionDelta": 1640 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 2744, + "afterCompletionDelta": 2676 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 1652, + "afterCompletionDelta": 1664 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": -3344, + "afterCompletionDelta": -3332 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 1884, + "afterCompletionDelta": 1652 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 8004, + "afterCompletionDelta": 6992 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 752, + "afterCompletionDelta": 508 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 672, + "afterCompletionDelta": 1464 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 520, + "afterCompletionDelta": 532 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 1548, + "afterCompletionDelta": 1560 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 776, + "afterCompletionDelta": 544 + } + ] +} diff --git a/docs/audits/terminal-mode-tail-retention/load-source.cjs b/docs/audits/terminal-mode-tail-retention/load-source.cjs new file mode 100644 index 00000000000..fdc39d2b3c6 --- /dev/null +++ b/docs/audits/terminal-mode-tail-retention/load-source.cjs @@ -0,0 +1,68 @@ +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 root = path.resolve(__dirname, '../../..') +const sha = (value) => createHash('sha256').update(value).digest('hex') +const read = (file) => fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n') +const versionsText = read(path.join(__dirname, 'source-versions.json')) +const versions = JSON.parse(versionsText) + +async function loadSource(fixed) { + const evaluatedSources = {} + const built = await build({ + stdin: { + contents: [ + "export { TerminalKittyKeyboardModeTracker } from './src/shared/terminal-kitty-keyboard-mode-tracker'", + "export { TerminalMouseModeMirror } from './src/main/daemon/terminal-mouse-mode-mirror'", + "export { ownRetainedString, resetOwnRetainedStringCopier } from './src/shared/own-retained-string'" + ].join('\n'), + resolveDir: root + }, + bundle: true, + platform: 'node', + format: 'cjs', + write: false, + plugins: [ + { + name: 'hash-fenced-retained-mode-tails', + setup(builder) { + builder.onLoad({ filter: /\.ts$/ }, ({ path: filename }) => { + const relative = path.relative(root, filename).split(path.sep).join('/') + const version = versions.sources[relative] + assert.ok(version, `Unreviewed source: ${relative}`) + let contents = read(filename) + assert.equal(sha(contents), version.fixedSha256, `Fixed source changed: ${relative}`) + if (!fixed && version.reverse) { + for (const { from, to, count } of version.reverse) { + assert.equal(contents.split(from).length - 1, count) + contents = contents.replaceAll(from, to) + } + } + const expected = fixed ? version.fixedSha256 : version.baselineSha256 + assert.equal(sha(contents), expected, `Evaluated source changed: ${relative}`) + evaluatedSources[relative] = sha(contents) + return { contents, loader: 'ts' } + }) + } + } + ] + }) + assert.deepEqual(Object.keys(evaluatedSources).sort(), Object.keys(versions.sources).sort()) + const filename = path.join(__dirname, fixed ? 'fixed-bundle.cjs' : 'baseline-bundle.cjs') + const loaded = new Module(filename, module) + loaded.filename = filename + loaded.paths = Module._nodeModulePaths(root) + loaded._compile(built.outputFiles[0].text, filename) + return { + ...loaded.exports, + evaluatedSources, + bundleSha256: sha(built.outputFiles[0].text), + sourceVersionsSha256: sha(versionsText) + } +} + +module.exports = { loadSource, sha, read } diff --git a/docs/audits/terminal-mode-tail-retention/node-results.json b/docs/audits/terminal-mode-tail-retention/node-results.json new file mode 100644 index 00000000000..e43d49c58df --- /dev/null +++ b/docs/audits/terminal-mode-tail-retention/node-results.json @@ -0,0 +1,467 @@ +{ + "node": "v26.6.0", + "electron": null, + "v8": "14.6.202.34-node.26", + "platform": "darwin", + "runnerSha256": "b78dc54a08345732235d4fcc1f72ca1534b00f89280981beda00667d22b8f291", + "loaderSha256": "730da9091abba3997432657aef4a2e6e7a3dd0e9218ea78f1838806b6c0fa789", + "pendingTailCodeUnits": 18, + "clearedRegexStatics": true, + "bundles": { + "baseline": { + "bundleSha256": "6023f5d6868f5db601f6883acda5d56990d204084a6a3c812799aff1e533a5b0", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-kitty-keyboard-mode-tracker.ts": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225" + }, + "sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a" + }, + "fixed-buffer": { + "bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225" + }, + "sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a" + }, + "fixed-fallback": { + "bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45", + "evaluatedSources": { + "src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84", + "src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225" + }, + "sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a" + } + }, + "reports": [ + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 2120952, + "afterCompletionDelta": 31112 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 33557944, + "afterCompletionDelta": 3840 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 2107000, + "afterCompletionDelta": 9336 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 33557928, + "afterCompletionDelta": 3752 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 3320, + "afterCompletionDelta": 3288 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 3176, + "afterCompletionDelta": 4792 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 3176, + "afterCompletionDelta": 4904 + }, + { + "variant": "baseline", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 33557944, + "afterCompletionDelta": 3232 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 2111984, + "afterCompletionDelta": 13904 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 33557072, + "afterCompletionDelta": 2272 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 1232, + "afterCompletionDelta": 1232 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 992, + "afterCompletionDelta": 1008 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 992, + "afterCompletionDelta": 1008 + }, + { + "variant": "baseline", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 33555840, + "afterCompletionDelta": 1048 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 18376, + "afterCompletionDelta": 25472 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 3448, + "afterCompletionDelta": 3144 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 9400, + "afterCompletionDelta": 8216 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 3696, + "afterCompletionDelta": 3392 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 3320, + "afterCompletionDelta": 3240 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 3176, + "afterCompletionDelta": 4648 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 3128, + "afterCompletionDelta": 3144 + }, + { + "variant": "fixed-buffer", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 3448, + "afterCompletionDelta": 3152 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 15112, + "afterCompletionDelta": 14376 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 1312, + "afterCompletionDelta": 1008 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 1232, + "afterCompletionDelta": 1200 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 992, + "afterCompletionDelta": 1008 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 992, + "afterCompletionDelta": 1008 + }, + { + "variant": "fixed-buffer", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 1344, + "afterCompletionDelta": 1048 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 9480, + "afterCompletionDelta": 8216 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 3448, + "afterCompletionDelta": 4048 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 13216, + "afterCompletionDelta": 11952 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scanReplay", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 3448, + "afterCompletionDelta": 3144 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 3320, + "afterCompletionDelta": 3240 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 3176, + "afterCompletionDelta": 3192 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 3912, + "afterCompletionDelta": 3928 + }, + { + "variant": "fixed-fallback", + "kind": "kitty", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 3448, + "afterCompletionDelta": 3152 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 65536, + "count": 32, + "expectedTailLength": 18, + "heapDelta": 9336, + "afterCompletionDelta": 8072 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 18, + "heapDelta": 1312, + "afterCompletionDelta": 1008 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 2, + "heapDelta": 1232, + "afterCompletionDelta": 8184 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 992, + "afterCompletionDelta": 1008 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 0, + "heapDelta": 2280, + "afterCompletionDelta": 2296 + }, + { + "variant": "fixed-fallback", + "kind": "mouse", + "method": "scan", + "inputChars": 4194304, + "count": 8, + "expectedTailLength": 17, + "heapDelta": 1312, + "afterCompletionDelta": 1016 + } + ] +} diff --git a/docs/audits/terminal-mode-tail-retention/reproduce.cjs b/docs/audits/terminal-mode-tail-retention/reproduce.cjs new file mode 100644 index 00000000000..61801919b7d --- /dev/null +++ b/docs/audits/terminal-mode-tail-retention/reproduce.cjs @@ -0,0 +1,205 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const { loadSource, sha, read } = require('./load-source.cjs') + +assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1') +assert.equal(typeof global.gc, 'function', 'Run with --expose-gc') +const pending = '\x1b[?1049;2004;1000;' +const sizes = [ + [64 * 1024, 32], + [4 * 1024 * 1024, 8] +] + +async function heap() { + // Isolate owner storage from V8's process-wide last successful regexp input. + ;/reset/.test('reset') + for (let round = 0; round < 4; round++) { + await new Promise((resolve) => setImmediate(resolve)) + global.gc() + } + return process.memoryUsage().heapUsed +} + +function createOwner(Owner, method, chars, index, suffix) { + const prefix = `${index}:` + const data = `${prefix}${'x'.repeat(chars - prefix.length - suffix.length)}${suffix}` + const owner = new Owner() + owner[method](data) + return owner +} + +async function measure(Owner, variant, kind, method, inputChars, count, suffix = pending) { + const beforeHeap = await heap() + const owners = Array.from({ length: count }, (_, index) => + createOwner(Owner, method, inputChars, index, suffix) + ) + const heapDelta = (await heap()) - beforeHeap + const expectedTailLength = suffix.length > 4096 || suffix.endsWith('h') ? 0 : suffix.length + assert.ok(owners.every((owner) => owner.scanTail.length === expectedTailLength)) + const retainsParent = variant === 'baseline' && expectedTailLength >= 13 + assert.ok( + retainsParent ? heapDelta > inputChars * count * 0.75 : heapDelta < 768 * 1024, + JSON.stringify({ variant, kind, method, inputChars, count, expectedTailLength, heapDelta }) + ) + + for (const owner of owners) { + owner[method]('1006h') + if (kind === 'kitty') { + if (suffix === pending) { + assert.equal(owner.isAlternateScreen, true) + } + owner[method]('\x1b[>3u') + assert.equal(owner.flags, 3) + owner.scan('\x1b[= 13) { + assert.equal(owner.mouseTrackingMode, 'vt200') + assert.equal(owner.sgrMouseMode, true) + } + assert.equal(owner.scanTail, '') + } + const afterCompletionDelta = (await heap()) - beforeHeap + assert.ok( + afterCompletionDelta < 768 * 1024, + JSON.stringify({ variant, kind, method, afterCompletionDelta }) + ) + for (const owner of owners) { + if (kind === 'kitty') { + owner.resetForSnapshot() + assert.equal(owner.snapshotFlags, undefined) + } else { + owner.scan('\x1bc') + assert.equal(owner.mouseTrackingMode, 'none') + assert.equal(owner.sgrMouseMode, false) + } + } + return { + variant, + kind, + method, + inputChars, + count, + expectedTailLength, + heapDelta, + afterCompletionDelta + } +} + +function configureCopier(api, fallback) { + api.resetOwnRetainedStringCopier() + const originalBuffer = globalThis.Buffer + try { + if (fallback) { + globalThis.Buffer = undefined + } + assert.equal(api.ownRetainedString(pending), pending) + } finally { + globalThis.Buffer = originalBuffer + } +} + +function behavior(Tracker, Mirror) { + const replay = new Tracker() + for (let index = 0; index < 70; index++) { + replay.scanReplay('\x1b[>3u') + } + assert.equal(replay.mainStack.length, 0) + assert.equal(replay.flags, 3) + replay.scan('\x1b[3u') + } + assert.equal(live.mainStack.length, 16) + live.scan('\x1b[?1049h\x1b[>5u') + assert.equal(live.altStack.length, 1) + live.scan('\x1b[?1049l') + assert.equal(live.flags, 3) + live.scan(`\x1bc${pending}`) + assert.equal(live.flags, 0) + assert.equal(live.scanTail, pending) + live.scan('1006h') + assert.equal(live.isAlternateScreen, true) + live.reset() + assert.equal(live.scanTail, '') + assert.equal(live.snapshotFlags, 0) + + const mouse = new Mirror() + mouse.scan('\x1b[?1003;1016h') + assert.equal(mouse.mouseTrackingMode, 'any') + assert.equal(mouse.sgrMousePixelsMode, true) + mouse.scan('\x9b?1002;1006h') + assert.equal(mouse.mouseTrackingMode, 'drag') + assert.equal(mouse.sgrMouseMode, true) + assert.equal(mouse.sgrMousePixelsMode, false) + mouse.scan(`\x1bc${pending}`) + assert.equal(mouse.mouseTrackingMode, 'none') + assert.equal(mouse.scanTail, pending) + mouse.scan('1006h') + assert.equal(mouse.mouseTrackingMode, 'vt200') + assert.equal(mouse.sgrMouseMode, true) +} + +async function main() { + const reports = [] + const bundles = {} + for (const variant of ['baseline', 'fixed-buffer', 'fixed-fallback']) { + const api = await loadSource(variant !== 'baseline') + const { TerminalKittyKeyboardModeTracker: Tracker, TerminalMouseModeMirror: Mirror } = api + bundles[variant] = { + bundleSha256: api.bundleSha256, + evaluatedSources: api.evaluatedSources, + sourceVersionsSha256: api.sourceVersionsSha256 + } + configureCopier(api, variant === 'fixed-fallback') + behavior(Tracker, Mirror) + for (const [kind, Owner, methods] of [ + ['kitty', Tracker, ['scan', 'scanReplay']], + ['mouse', Mirror, ['scan']] + ]) { + for (const method of methods) { + for (const [chars, count] of sizes) { + reports.push(await measure(Owner, variant, kind, method, chars, count)) + } + } + for (const suffix of [ + '\x1b[', + '\x1b[?1049;2004;1000;1006h', + `\x1b[${'1'.repeat(4095)}`, + pending.replace('\x1b[', '\x9b') + ]) { + reports.push(await measure(Owner, variant, kind, 'scan', 4 * 1024 * 1024, 8, suffix)) + } + } + } + const report = { + node: process.version, + electron: process.versions.electron ?? null, + v8: process.versions.v8, + platform: process.platform, + runnerSha256: sha(read(__filename)), + loaderSha256: sha(read(path.join(__dirname, 'load-source.cjs'))), + pendingTailCodeUnits: pending.length, + clearedRegexStatics: true, + bundles, + reports + } + const output = path.join( + __dirname, + `${process.versions.electron ? 'electron' : 'node'}-results.json` + ) + fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`) + console.log( + JSON.stringify({ output, passed: reports.length, node: report.node, electron: report.electron }) + ) +} +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) +setTimeout(() => { + console.error('fixture deadline') + process.exit(2) +}, 30000).unref() diff --git a/docs/audits/terminal-mode-tail-retention/source-versions.json b/docs/audits/terminal-mode-tail-retention/source-versions.json new file mode 100644 index 00000000000..fe29f805765 --- /dev/null +++ b/docs/audits/terminal-mode-tail-retention/source-versions.json @@ -0,0 +1,44 @@ +{ + "publicationTopic": "np-oom-scan-retained-text-slices", + "publicationBaseCommit": "8d599520e44654a5c28e9930e3070c00d6499931", + "historicalParserRef": "v1.4.198", + "historicalScope": "Both parser modules and kitty flag parser match this ref; owned-string helpers come from the publication topic. This is not a historical app binary.", + "sources": { + "src/shared/terminal-kitty-keyboard-mode-tracker.ts": { + "baselineSha256": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403", + "fixedSha256": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84", + "reverse": [ + { + "from": "import { ownRetainedString } from './own-retained-string'\n", + "to": "", + "count": 1 + }, + { "from": "? ownRetainedString(tail) : ''", "to": "? tail : ''", "count": 1 } + ] + }, + "src/main/daemon/terminal-mouse-mode-mirror.ts": { + "baselineSha256": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1", + "fixedSha256": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3", + "reverse": [ + { + "from": "import { ownRetainedString } from '../../shared/own-retained-string'\n", + "to": "", + "count": 1 + }, + { "from": "? ownRetainedString(tail) : ''", "to": "? tail : ''", "count": 2 } + ] + }, + "src/shared/own-retained-string.ts": { + "baselineSha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2", + "fixedSha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2" + }, + "src/shared/owned-utf16-suffix.ts": { + "baselineSha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475", + "fixedSha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475" + }, + "src/shared/terminal-kitty-keyboard-flags.ts": { + "baselineSha256": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225", + "fixedSha256": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225" + } + } +} diff --git a/src/main/claude/claude-background-task-frames.ts b/src/main/claude/claude-background-task-frames.ts index c34d8224cef..7be5b190588 100644 --- a/src/main/claude/claude-background-task-frames.ts +++ b/src/main/claude/claude-background-task-frames.ts @@ -8,6 +8,7 @@ import type { AgentSessionBackgroundTaskRunState } from '../../shared/agent-session-wire' import { backgroundTaskFallbackText } from '../../shared/native-chat-background-task-row' +import { ownRetainedString } from '../../shared/own-retained-string' const MAX_TASK_ID_LENGTH = 512 const MAX_TASK_TEXT_LENGTH = 512 @@ -39,7 +40,7 @@ function boundedTaskText(value: unknown): string | undefined { return undefined } const trimmed = value.trim().replace(/\s+/g, ' ') - return trimmed.length > 0 ? trimmed.slice(0, MAX_TASK_TEXT_LENGTH) : undefined + return trimmed.length > 0 ? ownRetainedString(trimmed.slice(0, MAX_TASK_TEXT_LENGTH)) : undefined } export function taskDescription(value: unknown): string | undefined { diff --git a/src/main/claude/claude-background-task-retention.test.ts b/src/main/claude/claude-background-task-retention.test.ts new file mode 100644 index 00000000000..f5917cb3d45 --- /dev/null +++ b/src/main/claude/claude-background-task-retention.test.ts @@ -0,0 +1,96 @@ +import { setImmediate } from 'node:timers/promises' +import { expect, it } from 'vitest' +import { ClaudeBackgroundTaskTracker } from './claude-background-task-tracker' +import { taskDescription, taskName } from './claude-background-task-frames' + +type Retention = 'live' | 'settled' | 'removed' +type Field = 'description' | 'name' +const TASKS = 8 +const INPUT_CHARS = 1024 * 1024 + +function collectHeap(): number { + const collect = globalThis.gc + if (typeof collect !== 'function') { + throw new Error('global.gc unavailable: run with the repository Vitest --expose-gc config') + } + for (let index = 0; index < 3; index++) { + collect() + } + return process.memoryUsage().heapUsed +} + +function populate(field: Field, retention: Retention, count = TASKS): ClaudeBackgroundTaskTracker { + const tracker = new ClaudeBackgroundTaskTracker(() => 1) + const keeper = { + type: 'system', + subtype: 'task_started', + task_id: 'keeper', + task_type: 'local_bash', + is_backgrounded: true + } + tracker.observe(keeper) + for (let index = 0; index < count; index++) { + tracker.observe( + JSON.parse( + JSON.stringify({ + ...keeper, + task_id: `task-${index}`, + [field]: String.fromCharCode(65 + index).repeat(INPUT_CHARS) + }) + ) + ) + if (retention === 'settled') { + tracker.observe({ + type: 'system', + subtype: 'task_notification', + task_id: `task-${index}`, + status: 'completed' + }) + } + } + if (retention === 'removed') { + tracker.observe({ type: 'system', subtype: 'background_tasks_changed', tasks: [keeper] }) + } + return tracker +} + +it.each([ + ['description', 'live'], + ['description', 'settled'], + ['description', 'removed'], + ['name', 'live'], + ['name', 'settled'], + ['name', 'removed'] +] as const)('owns bounded %s text retained by %s tasks', async (field, retention) => { + populate(field, retention, 1).clear() + await setImmediate() + const before = collectHeap() + const tracker = populate(field, retention) + await setImmediate() + try { + expect(collectHeap() - before).toBeLessThan(2 * 1024 * 1024) + if (retention === 'live') { + expect(tracker.state?.tasks?.find((task) => task.id === 'task-0')?.[field]).toBe( + 'A'.repeat(512) + ) + } else if (retention === 'settled') { + expect(tracker.state?.settledTasks?.find((task) => task.id === 'task-0')?.[field]).toBe( + 'A'.repeat(512) + ) + } else { + expect(tracker.state?.tasks?.map((task) => task.id)).toEqual(['keeper']) + } + } finally { + tracker.clear() + } +}) + +it('preserves normalization, name fallback, and the UTF-16 clipping boundary', () => { + expect(taskDescription(' \t run\n the\r\n build ')).toBe('run the build') + expect(taskDescription(' \t\r\n ')).toBeUndefined() + expect(taskDescription(null)).toBeUndefined() + expect(taskName({ name: ' ', agent_type: '\t reviewer\nagent ' })).toBe('reviewer agent') + const value = `${'漢'.repeat(511)}\ud83d\ude00\udfff` + expect(taskDescription(value)).toBe(value.slice(0, 512)) + expect(taskName({ subagent_type: value })).toBe(value.slice(0, 512)) +}) diff --git a/src/main/daemon/terminal-mouse-mode-mirror.ts b/src/main/daemon/terminal-mouse-mode-mirror.ts index 8f8b284f7f4..7e2ea703cac 100644 --- a/src/main/daemon/terminal-mouse-mode-mirror.ts +++ b/src/main/daemon/terminal-mouse-mode-mirror.ts @@ -1,3 +1,4 @@ +import { ownRetainedString } from '../../shared/own-retained-string' import type { TerminalModes } from './types' type MouseTrackingMode = NonNullable @@ -105,10 +106,10 @@ export class TerminalMouseModeMirror { return tail } if (tail.startsWith('\x1b[?')) { - return this.isIncompleteParams(tail.slice(3)) ? tail : '' + return this.isIncompleteParams(tail.slice(3)) ? ownRetainedString(tail) : '' } if (tail.startsWith('\x9b?')) { - return this.isIncompleteParams(tail.slice(2)) ? tail : '' + return this.isIncompleteParams(tail.slice(2)) ? ownRetainedString(tail) : '' } return '' } diff --git a/src/main/daemon/terminal-mouse-tail-retention.test.ts b/src/main/daemon/terminal-mouse-tail-retention.test.ts new file mode 100644 index 00000000000..7a266b4b23d --- /dev/null +++ b/src/main/daemon/terminal-mouse-tail-retention.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' +import { TerminalMouseModeMirror } from './terminal-mouse-mode-mirror' + +function heapAfterGc(): number { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + // Isolate mirror ownership from V8's process-wide last successful regexp input. + void /reset/.test('reset') + globalThis.gc() + globalThis.gc() + return process.memoryUsage().heapUsed +} + +describe('mouse mode scan tail retention', () => { + it.each(['\x1b[', '\x9b'])( + 'retains a split %j mode sequence without retaining consumed output', + (introducer) => { + const before = heapAfterGc() + const mirrors = Array.from({ length: 8 }, (_value, index) => { + const mirror = new TerminalMouseModeMirror() + mirror.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}${introducer}?1049;2004;1000;`) + return mirror + }) + + expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024) + for (const mirror of mirrors) { + expect(mirror.mouseTrackingMode).toBe('none') + mirror.scan('1006h') + expect(mirror.mouseTrackingMode).toBe('vt200') + expect(mirror.sgrMouseMode).toBe(true) + mirror.scan('\x1b[?1016h') + expect(mirror.sgrMouseMode).toBe(false) + expect(mirror.sgrMousePixelsMode).toBe(true) + mirror.scan('\x1bc') + expect(mirror.mouseTrackingMode).toBe('none') + expect(mirror.sgrMousePixelsMode).toBe(false) + } + } + ) +}) diff --git a/src/main/plugins/plugin-worker-output-buffer.ts b/src/main/plugins/plugin-worker-output-buffer.ts index 836330c8799..6a0cb2a078b 100644 --- a/src/main/plugins/plugin-worker-output-buffer.ts +++ b/src/main/plugins/plugin-worker-output-buffer.ts @@ -1,4 +1,5 @@ import type { Readable } from 'node:stream' +import { ownRetainedString } from '../../shared/own-retained-string' type PluginWorkerOutputSink = (level: 'info' | 'warn' | 'error', line: string) => void @@ -21,9 +22,11 @@ export function pipePluginWorkerOutput( if (line.trim().length > 0) { log( level, - truncated - ? `${line.slice(0, PLUGIN_WORKER_OUTPUT_LINE_LIMIT - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}` - : line + ownRetainedString( + truncated + ? `${line.slice(0, PLUGIN_WORKER_OUTPUT_LINE_LIMIT - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}` + : line + ) ) } } @@ -49,7 +52,7 @@ export function pipePluginWorkerOutput( buffered = '' discarding = newline === -1 } else { - buffered += segment + buffered += newline === -1 ? ownRetainedString(segment) : segment if (newline !== -1) { emit(buffered) buffered = '' diff --git a/src/main/plugins/plugin-worker-output-retention.test.ts b/src/main/plugins/plugin-worker-output-retention.test.ts new file mode 100644 index 00000000000..35784541f74 --- /dev/null +++ b/src/main/plugins/plugin-worker-output-retention.test.ts @@ -0,0 +1,82 @@ +import { once } from 'node:events' +import { PassThrough } from 'node:stream' +import { describe, expect, it } from 'vitest' +import { PluginLogBuffer } from './plugin-log-buffer' +import { pipePluginWorkerOutput } from './plugin-worker-output-buffer' + +async function heapAfterGc(): Promise { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + for (let round = 0; round < 3; round++) { + await new Promise((resolve) => setImmediate(resolve)) + globalThis.gc() + } + return process.memoryUsage().heapUsed +} + +async function endStream(stream: PassThrough): Promise { + const ended = once(stream, 'end') + stream.end() + await ended +} + +function writeTail(stream: PassThrough, index: number): void { + stream.write(`${' '.repeat(4 * 1024 * 1024)}\nretained output ${index}`) +} + +function writeLine(stream: PassThrough, index: number, truncated: boolean): void { + const prefix = String(index).padStart(4, '0') + stream.write( + truncated + ? `${prefix}${'x'.repeat(64 * 1024)}\n` + : `${' '.repeat(64 * 1024)}\nretained output ${prefix}\n` + ) +} + +describe('plugin worker retained output', () => { + it('keeps unfinished output after consuming a large chunk without retaining the parent', async () => { + const lines: string[] = [] + const before = await heapAfterGc() + const streams = Array.from({ length: 8 }, (_value, index) => { + const stream = new PassThrough() + pipePluginWorkerOutput(stream, 'info', (_level, line) => lines.push(line)) + writeTail(stream, index) + return stream + }) + + expect((await heapAfterGc()) - before).toBeLessThan(2 * 1024 * 1024) + expect(lines).toEqual([]) + for (const stream of streams) { + await endStream(stream) + } + expect(lines).toEqual(Array.from({ length: 8 }, (_value, index) => `retained output ${index}`)) + }) + + it.each([false, true])( + 'owns emitted log text without retaining consumed chunks (truncated=%s)', + async (truncated) => { + const logs = new PluginLogBuffer() + const stream = new PassThrough() + pipePluginWorkerOutput(stream, 'error', (level, line) => logs.append('plugin', level, line)) + const before = await heapAfterGc() + for (let index = 0; index < 205; index++) { + writeLine(stream, index, truncated) + } + await endStream(stream) + + // Compare text after the heap check: comparisons can flatten concatenated strings. + expect((await heapAfterGc()) - before).toBeLessThan(5 * 1024 * 1024) + expect(logs.get('plugin')).toHaveLength(200) + for (const [index, row] of logs.get('plugin').entries()) { + const prefix = String(index + 5).padStart(4, '0') + expect(row.level).toBe('error') + expect(row.line).toBe( + truncated + ? `${prefix}${'x'.repeat(8192 - 4 - '… [truncated]'.length)}… [truncated]` + : `retained output ${prefix}` + ) + } + } + ) +}) diff --git a/src/main/ports/advertised-url-parsing.ts b/src/main/ports/advertised-url-parsing.ts index 1d342e8e03b..f50a1ff253f 100644 --- a/src/main/ports/advertised-url-parsing.ts +++ b/src/main/ports/advertised-url-parsing.ts @@ -1,4 +1,5 @@ /* eslint-disable no-control-regex -- Terminal control-sequence parsing intentionally matches raw control bytes. */ +import { ownRetainedString } from '../../shared/own-retained-string' import type { AdvertisedUrl, AdvertisedUrlChangeEvent, @@ -45,7 +46,7 @@ export class PtyBuffer { const chunkHasLineBreak = chunk.includes('\n') || chunk.includes('\r') // Keep the suffix directly so oversized chunks never materialize a throwaway full concatenation. if (chunk.length >= PER_PTY_BUFFER_LIMIT) { - this.raw = chunk.slice(-PER_PTY_BUFFER_LIMIT) + this.raw = ownRetainedString(chunk.slice(-PER_PTY_BUFFER_LIMIT)) } else if (this.raw.length + chunk.length > PER_PTY_BUFFER_LIMIT) { this.raw = `${this.raw.slice(-(PER_PTY_BUFFER_LIMIT - chunk.length))}${chunk}` } else { diff --git a/src/main/ports/advertised-url-retention.test.ts b/src/main/ports/advertised-url-retention.test.ts new file mode 100644 index 00000000000..9b5cf25b5cf --- /dev/null +++ b/src/main/ports/advertised-url-retention.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { AdvertisedUrlWatcher } from './advertised-url-watcher' + +function heapAfterGc(): number { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + globalThis.gc() + globalThis.gc() + return process.memoryUsage().heapUsed +} + +function ingestOversizedOutput(watcher: AdvertisedUrlWatcher, bound: boolean): void { + for (let index = 0; index < 8; index++) { + const ptyId = `pty-${index}` + if (bound) { + watcher.bindPty(ptyId, 'workspace') + } + watcher.ingest( + ptyId, + `${index}:${'x'.repeat(4 * 1024 * 1024)}\nhttp://localhost:${4100 + index}` + ) + } +} + +describe('advertised URL output retention', () => { + it.each([true, false])('releases oversized parents with PTYs bound=%s', (bound) => { + const watcher = new AdvertisedUrlWatcher() + const before = heapAfterGc() + ingestOversizedOutput(watcher, bound) + expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024) + + for (let index = 0; index < 8; index++) { + const ptyId = `pty-${index}` + watcher.bindPty(ptyId, 'workspace') + watcher.ingest(ptyId, '/\n') + expect(watcher.lookup('workspace', 4100 + index)?.origin).toBe( + `http://localhost:${4100 + index}` + ) + watcher.unbindPty(ptyId) + expect(watcher.lookup('workspace', 4100 + index)).toBeUndefined() + } + }) +}) diff --git a/src/main/ports/advertised-url-watcher.ts b/src/main/ports/advertised-url-watcher.ts index ebbbecdf25f..c26c3e2b9ce 100644 --- a/src/main/ports/advertised-url-watcher.ts +++ b/src/main/ports/advertised-url-watcher.ts @@ -20,6 +20,7 @@ import { lookupBestAdvertisedUrl, shouldEvictAdvertisedUrlAfterScan } from './advertised-url-reconciliation' +import { ownRetainedString } from '../../shared/own-retained-string' export type HostKind = 'custom' | 'loopback' | 'private-ip' | 'public-ip' export type AdvertisedUrl = { @@ -140,7 +141,11 @@ export class AdvertisedUrlWatcher { if (!worktreeId) { // Why: daemon PTY data can arrive before the spawn handler resolves the worktreeId (src/main/ipc/pty.ts:1318-1323); buffer until bindPty replays. const prior = this.pending.get(ptyId) ?? '' - const merged = (prior + chunk).slice(-PENDING_PRE_BIND_LIMIT) + const combined = prior + chunk + const merged = + combined.length > PENDING_PRE_BIND_LIMIT + ? ownRetainedString(combined.slice(-PENDING_PRE_BIND_LIMIT)) + : combined // Why: drop+reinsert refreshes Map insertion order (LRU) so the eviction below drops the oldest unbound PTY. this.pending.delete(ptyId) this.pending.set(ptyId, merged) diff --git a/src/main/runtime/recent-pty-output-buffer.ts b/src/main/runtime/recent-pty-output-buffer.ts index dda01e0afdf..7c067913a93 100644 --- a/src/main/runtime/recent-pty-output-buffer.ts +++ b/src/main/runtime/recent-pty-output-buffer.ts @@ -1,3 +1,5 @@ +import { ownRetainedString } from '../../shared/own-retained-string' + export const RECENT_PTY_OUTPUT_LIMIT = 64 * 1024 // Compact the backing array once this many fully-dropped head slots accumulate, @@ -42,7 +44,7 @@ export class RecentPtyOutputBuffer { return } if (data.length >= this.limit) { - this.chunks = [data.slice(-this.limit)] + this.chunks = [data.length > this.limit ? ownRetainedString(data.slice(-this.limit)) : data] this.headIndex = 0 this.headOffset = 0 this.totalLen = this.limit diff --git a/src/main/runtime/recent-pty-output-retention.test.ts b/src/main/runtime/recent-pty-output-retention.test.ts new file mode 100644 index 00000000000..b5207836e6c --- /dev/null +++ b/src/main/runtime/recent-pty-output-retention.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { RECENT_PTY_OUTPUT_LIMIT, RecentPtyOutputBuffer } from './recent-pty-output-buffer' + +function heapAfterGc(): number { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + globalThis.gc() + globalThis.gc() + return process.memoryUsage().heapUsed +} + +describe('recent PTY output retention', () => { + it.each([true, false])( + 'releases oversized parent strings with boundary preservation=%s', + (preserveChunkBoundaries) => { + const count = 8 + const before = heapAfterGc() + const buffers = Array.from({ length: count }, (_value, index) => { + const buffer = new RecentPtyOutputBuffer({ preserveChunkBoundaries }) + buffer.append(`${index}:${'x'.repeat(4 * 1024 * 1024)}`) + return buffer + }) + const growth = heapAfterGc() - before + + expect(growth).toBeLessThan(count * RECENT_PTY_OUTPUT_LIMIT * 4) + for (const buffer of buffers) { + expect(buffer.read()).toBe('x'.repeat(RECENT_PTY_OUTPUT_LIMIT)) + expect(buffer.retainedChunks().headChunkIsPartial).toBe(true) + buffer.append('next') + expect(buffer.read()).toBe(`${'x'.repeat(RECENT_PTY_OUTPUT_LIMIT - 4)}next`) + } + } + ) +}) diff --git a/src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue.ts b/src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue.ts index 3045569b1e1..86c2f6fe4fd 100644 --- a/src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue.ts +++ b/src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue.ts @@ -1,4 +1,5 @@ import type { PtyDataMeta } from './pty-dispatcher' +import { flattenRetainedSlice } from '../../lib/flatten-retained-slice' export const MAX_DEFERRED_REATTACH_LIVE_CHARS = 512 * 1024 export const MAX_DEFERRED_REATTACH_LIVE_CHUNKS = 1_024 @@ -35,7 +36,9 @@ export class DeferredReattachLiveDataQueue { const oversized = chunk.data.length > MAX_DEFERRED_REATTACH_LIVE_CHARS const queuedChunk = { ...chunk, - data: oversized ? chunk.data.slice(-MAX_DEFERRED_REATTACH_LIVE_CHARS) : chunk.data + data: oversized + ? flattenRetainedSlice(chunk.data.slice(-MAX_DEFERRED_REATTACH_LIVE_CHARS)) + : chunk.data } this.chunks.push(queuedChunk) this.retainedChars += queuedChunk.data.length diff --git a/src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp.ts b/src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp.ts index 0d4087e8288..90a8e122303 100644 --- a/src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp.ts +++ b/src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp.ts @@ -1,4 +1,5 @@ import { clampUtf8TextTail } from '../../../../shared/utf8-byte-limits' +import { flattenRetainedSlice } from '../../lib/flatten-retained-slice' export type EagerBufferChunk = { data: string @@ -7,5 +8,8 @@ export type EagerBufferChunk = { export function clampUtf8Tail(data: string, maxBytes: number): EagerBufferChunk { const tail = clampUtf8TextTail(data, maxBytes) - return { data: tail.text, bytes: tail.bytes } + return { + data: tail.text.length < data.length ? flattenRetainedSlice(tail.text) : tail.text, + bytes: tail.bytes + } } diff --git a/src/renderer/src/components/terminal-pane/terminal-capped-buffer-retention.test.ts b/src/renderer/src/components/terminal-pane/terminal-capped-buffer-retention.test.ts new file mode 100644 index 00000000000..5d16aeb6979 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-capped-buffer-retention.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest' +import { capTerminalScrollbackSessionBuffer } from '../../../../shared/workspace-session-terminal-buffers' +import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from '../../../../shared/terminal-scrollback-limits' +import { clampUtf8Tail } from './pty-eager-buffer-clamp' +import { PtyShutdownOutputQueue } from './pty-shutdown-output-queue' +import { DeferredReattachLiveDataQueue } from './deferred-reattach-live-data-queue' +import { appendPaneTerminalError, type TerminalErrorsByPaneId } from './terminal-error-accumulation' + +const LIMIT = TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT +const PARENT_CHARS = 4 * 1024 * 1024 +const COUNT = 8 + +function heapAfterGc(): number { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + globalThis.gc() + globalThis.gc() + return process.memoryUsage().heapUsed +} + +function createPaneErrors(): TerminalErrorsByPaneId { + let errors: TerminalErrorsByPaneId = {} + for (let index = 0; index < COUNT; index++) { + errors = appendPaneTerminalError(errors, 0, `${'x'.repeat(PARENT_CHARS)}:${index}`) + } + return errors +} + +describe('capped terminal buffer retention', () => { + it.each([ + ['persisted scrollback', capTerminalScrollbackSessionBuffer], + ['eager/pre-handler output', (text: string) => clampUtf8Tail(text, LIMIT).data] + ] as const)('detaches %s from oversized incoming strings', (_label, cap) => { + const before = heapAfterGc() + const retained = Array.from({ length: COUNT }, (_value, index) => + cap(`${index}:${'x'.repeat(PARENT_CHARS)}`) + ) + const growth = heapAfterGc() - before + + expect(retained.every((text) => text === 'x'.repeat(LIMIT))).toBe(true) + expect(growth).toBeLessThan(COUNT * LIMIT * 2) + }) + + it('keeps shutdown queue heap storage near its byte ledger after clamping', () => { + const before = heapAfterGc() + const queues = Array.from({ length: COUNT }, (_value, index) => { + const queue = new PtyShutdownOutputQueue() + queue.enqueue({ kind: 'replay', data: `${index}:${'x'.repeat(PARENT_CHARS)}` }) + return queue + }) + const growth = heapAfterGc() - before + + expect(queues.every((queue) => queue.getStorageForTest().retainedBytes === LIMIT)).toBe(true) + expect(growth).toBeLessThan(COUNT * LIMIT * 2) + for (const queue of queues) { + expect(queue.takeAll()).toEqual([{ kind: 'replay', data: 'x'.repeat(LIMIT) }]) + } + }) + + it('detaches oversized chunks while a reattach queue waits for its consumer', () => { + const before = heapAfterGc() + const queues = Array.from({ length: COUNT }, (_value, index) => { + const queue = new DeferredReattachLiveDataQueue() + queue.enqueue({ + data: `${index}:${'x'.repeat(PARENT_CHARS)}`, + ptyId: 'p', + streamGeneration: 1 + }) + return queue + }) + const growth = heapAfterGc() - before + + expect(queues.every((queue) => queue.getStorageForTest().retainedChars === LIMIT)).toBe(true) + expect(growth).toBeLessThan(COUNT * LIMIT * 2) + for (const queue of queues) { + expect(queue.takeAll()[0]?.data).toBe('x'.repeat(LIMIT)) + } + }) + + it('keeps capped pane errors without retaining the original error payloads', () => { + const before = heapAfterGc() + const errors = createPaneErrors() + const growth = heapAfterGc() - before + + expect(errors[0]).toHaveLength(COUNT) + expect( + errors[0].every((text, index) => text.length === 4000 && text.endsWith(`:${index}`)) + ).toBe(true) + expect(growth).toBeLessThan(PARENT_CHARS) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/terminal-error-accumulation.ts b/src/renderer/src/components/terminal-pane/terminal-error-accumulation.ts index 63c5de8423e..d22364e74ad 100644 --- a/src/renderer/src/components/terminal-pane/terminal-error-accumulation.ts +++ b/src/renderer/src/components/terminal-pane/terminal-error-accumulation.ts @@ -1,3 +1,5 @@ +import { flattenRetainedSlice } from '../../lib/flatten-retained-slice' + // The toast still consumes newline-joined copy, so legacy tab-wide messages need // whole-run dedup even though pane errors remain structurally separate until render. function containsWholeLineRun(accumulated: string, message: string): boolean { @@ -22,12 +24,12 @@ export function boundTerminalErrorSurface( const lines = surface.split('\n') let bounded = lines.length > maxLines ? lines.slice(-maxLines).join('\n') : surface if (bounded.length <= maxChars) { - return bounded + return flattenRetainedSlice(bounded) } const suffix = bounded.slice(-maxChars) const firstNewline = suffix.indexOf('\n') bounded = firstNewline === -1 ? suffix : suffix.slice(firstNewline + 1) || suffix - return bounded + return flattenRetainedSlice(bounded) } export function appendPaneTerminalError( diff --git a/src/shared/check-job-log-retention.test.ts b/src/shared/check-job-log-retention.test.ts new file mode 100644 index 00000000000..a1ca9505140 --- /dev/null +++ b/src/shared/check-job-log-retention.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { PR_CHECK_LOG_TAIL_BYTES, sliceCheckLogTail } from './check-job-log-tail-slice' +import { gitLabJobTraceToLogExcerpt } from './gitlab-job-log-excerpt' + +function heapAfterGc(): number { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + globalThis.gc() + globalThis.gc() + return process.memoryUsage().heapUsed +} + +const PARENT_CHARS = 2 * 1024 * 1024 +const COUNT = 8 + +describe('retained CI log excerpts', () => { + it.each([ + [ + 'GitHub long line', + (index: number) => `${index}:${'x'.repeat(PARENT_CHARS)}`, + sliceCheckLogTail + ], + [ + 'GitHub earlier error', + (index: number) => `error: ${index}:${'界'.repeat(PARENT_CHARS)}\n${'recent\n'.repeat(100)}`, + sliceCheckLogTail + ], + [ + 'GitLab raw trace', + (index: number) => `${index}:${'x'.repeat(PARENT_CHARS)}`, + gitLabJobTraceToLogExcerpt + ] + ] as const)('releases the parent of a %s', (_label, makeLog, excerpt) => { + const before = heapAfterGc() + const retained = Array.from({ length: COUNT }, (_value, index) => excerpt(makeLog(index))) + // V8's legacy RegExp statics can otherwise keep the final input independently of our cache. + void /probe/.test('probe') + const growth = heapAfterGc() - before + + expect(retained).toHaveLength(COUNT) + expect(retained.every((text) => Buffer.byteLength(text) <= PR_CHECK_LOG_TAIL_BYTES)).toBe(true) + expect(growth).toBeLessThan(PARENT_CHARS * 2) + }) +}) diff --git a/src/shared/check-job-log-tail-slice.ts b/src/shared/check-job-log-tail-slice.ts index 19afcdfc75d..17b03190875 100644 --- a/src/shared/check-job-log-tail-slice.ts +++ b/src/shared/check-job-log-tail-slice.ts @@ -3,6 +3,7 @@ import { getUtf8ByteLength, isUtf8ByteLengthWithinLimit } from './utf8-byte-limits' +import { ownRetainedString } from './own-retained-string' export const PR_CHECK_LOG_TAIL_LINES = 200 export const PR_CHECK_LOG_TAIL_RECENT_LINES = 100 @@ -57,7 +58,7 @@ function collectEarlierErrorLineIndexes(lines: string[], recentStart: number): n return [...indexes].sort((left, right) => left - right) } -export function sliceCheckLogTail(logText: string): string { +function buildCheckLogTail(logText: string): string { const lines = logText.split(/\r?\n/) const recentStart = Math.max(0, lines.length - PR_CHECK_LOG_TAIL_RECENT_LINES) const recentLines = lines.slice(recentStart) @@ -80,3 +81,8 @@ export function sliceCheckLogTail(logText: string): string { recentLines ) } + +export function sliceCheckLogTail(logText: string): string { + // Cached excerpts must not pin the downloaded log behind a small V8 slice. + return ownRetainedString(buildCheckLogTail(logText)) +} diff --git a/src/shared/command-code-output-retention.test.ts b/src/shared/command-code-output-retention.test.ts new file mode 100644 index 00000000000..681d12270e5 --- /dev/null +++ b/src/shared/command-code-output-retention.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { createCommandCodeOutputStatusDetector } from './command-code-output-status' + +function heapAfterGc(): number { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + globalThis.gc() + globalThis.gc() + return process.memoryUsage().heapUsed +} + +describe('Command Code output retention', () => { + it('keeps small boundary carries without pinning oversized output on ordinary panes', () => { + const before = heapAfterGc() + const detectors = Array.from({ length: 8 }, (_value, index) => { + const detector = createCommandCodeOutputStatusDetector({ onWorking: () => {} }) + detector.observe(`${index}:${'x'.repeat(4 * 1024 * 1024)}`) + return detector + }) + expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024) + for (const detector of detectors) { + expect(detector.observe('\nordinary shell output\n')).toBe(false) + } + }) +}) diff --git a/src/shared/command-code-output-status.ts b/src/shared/command-code-output-status.ts index e98f72d857b..b5fb611a73d 100644 --- a/src/shared/command-code-output-status.ts +++ b/src/shared/command-code-output-status.ts @@ -11,6 +11,7 @@ import { } from './command-code-prompt-text' import { stripTerminalControl } from './terminal-control-stripping' import { escapeRegex } from './string-utils' +import { ownRetainedString } from './own-retained-string' export { stripTerminalControl } from './terminal-control-stripping' @@ -159,7 +160,7 @@ function rawChunkMayContainCommandCodeBanner(previousRawText: string, data: stri function appendRecentRawText(previousRawText: string, data: string): string { if (data.length >= RECENT_TEXT_LIMIT) { - return data.slice(-RECENT_TEXT_LIMIT) + return ownRetainedString(data.slice(-RECENT_TEXT_LIMIT)) } return (previousRawText + data).slice(-RECENT_TEXT_LIMIT) } diff --git a/src/shared/terminal-kitty-keyboard-mode-tracker.ts b/src/shared/terminal-kitty-keyboard-mode-tracker.ts index cae87067973..72c9d5b91f3 100644 --- a/src/shared/terminal-kitty-keyboard-mode-tracker.ts +++ b/src/shared/terminal-kitty-keyboard-mode-tracker.ts @@ -1,3 +1,4 @@ +import { ownRetainedString } from './own-retained-string' import { parseTerminalKittyKeyboardFlags } from './terminal-kitty-keyboard-flags' // Why: PTY/SSH chunks can split an escape sequence before its final byte. @@ -308,7 +309,7 @@ export class TerminalKittyKeyboardModeTracker { if (body === null) { return '' } - return this.isIncompleteSequenceBody(body) ? tail : '' + return this.isIncompleteSequenceBody(body) ? ownRetainedString(tail) : '' } private isIncompleteSequenceBody(body: string): boolean { diff --git a/src/shared/terminal-kitty-keyboard-tail-retention.test.ts b/src/shared/terminal-kitty-keyboard-tail-retention.test.ts new file mode 100644 index 00000000000..4c1b92acffd --- /dev/null +++ b/src/shared/terminal-kitty-keyboard-tail-retention.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' +import { TerminalKittyKeyboardModeTracker } from './terminal-kitty-keyboard-mode-tracker' + +const INCOMPLETE_MODE = '\x1b[?1049;2004;1000;' + +function heapAfterGc(): number { + if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') { + throw new Error('The test runner must enable --expose-gc') + } + // Isolate tracker ownership from V8's process-wide last successful regexp input. + void /reset/.test('reset') + globalThis.gc() + globalThis.gc() + return process.memoryUsage().heapUsed +} + +describe('kitty keyboard scan tail retention', () => { + it.each(['scan', 'scanReplay'] as const)( + '%s retains a split mode sequence without retaining consumed output', + (method) => { + const before = heapAfterGc() + const trackers = Array.from({ length: 8 }, (_value, index) => { + const tracker = new TerminalKittyKeyboardModeTracker() + tracker[method](`${index}:${'x'.repeat(4 * 1024 * 1024)}${INCOMPLETE_MODE}`) + return tracker + }) + + expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024) + for (const tracker of trackers) { + expect(tracker.isAlternateScreen).toBe(false) + tracker[method]('1006h\x1b[>3u') + expect(tracker.isAlternateScreen).toBe(true) + expect(tracker.flags).toBe(3) + tracker.scan('\x1b[ { + vi.unstubAllGlobals() + resetOwnRetainedStringCopier() +}) + +describe.each([false, true])('OSC 133 carry with Bufferless copying=%s', (withoutBuffer) => { + // Syntax from the captured fish 4.7.1 fixture in terminal-mode-2031-final-state.test.ts. + it.each([FISH_PROMPT, FISH_COMMAND])('owns a retained fish suffix %j', (suffix) => { + selectCopier(withoutBuffer) + const started = vi.fn() + const finished = vi.fn() + const before = heapAfterGc() + const scanners = Array.from({ length: 8 }, (_value, index) => { + const scanner = createOsc133CommandFinishedScanner(finished, started) + scanner.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}${suffix}`) + return scanner + }) + + expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024) + expect(started).not.toHaveBeenCalled() + expect(finished).not.toHaveBeenCalled() + for (const scanner of scanners) { + scanner.scan('\x07\x1b]133;D;137\x1b\\') + } + expect(started).toHaveBeenCalledTimes(suffix === FISH_COMMAND ? scanners.length : 0) + expect(finished.mock.calls).toEqual(Array.from({ length: scanners.length }, () => [137])) + expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024) + }) + + it('preserves UTF-16 carry at every split and retires a reset prefix', () => { + selectCopier(withoutBuffer) + for (let cut = 1; cut < UTF16_CARRY.length; cut += 1) { + const finished = vi.fn() + const scanner = createOsc133CommandFinishedScanner(finished) + scanner.scan(UTF16_CARRY.slice(0, cut)) + scanner.scan(`${UTF16_CARRY.slice(cut)}\x1b\\`) + expect(finished.mock.calls).toEqual([[1234567890]]) + scanner.scan(FISH_COMMAND) + scanner.reset() + scanner.scan('\x07') + expect(finished.mock.calls).toEqual([[1234567890]]) + } + }) +}) + +it('short command-finished carry does not retain consumed output and completes once', () => { + const finished = vi.fn() + const before = heapAfterGc() + const scanners = Array.from({ length: 8 }, (_value, index) => { + const scanner = createOsc133CommandFinishedScanner(finished) + scanner.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}\x1b]133;D;0`) + return scanner + }) + expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024) + for (const scanner of scanners) { + scanner.scan('\x07') + scanner.scan('\x07') + } + expect(finished.mock.calls).toEqual(Array.from({ length: scanners.length }, () => [0])) +}) + +it('reset releases a pending parent before its terminator arrives', () => { + const finished = vi.fn() + const started = vi.fn() + const before = heapAfterGc() + const scanners = Array.from({ length: 8 }, (_value, index) => { + const scanner = createOsc133CommandFinishedScanner(finished, started) + scanner.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}${FISH_COMMAND}`) + scanner.reset() + return scanner + }) + expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024) + for (const scanner of scanners) { + scanner.scan('\x07') + } + expect(started).not.toHaveBeenCalled() + expect(finished).not.toHaveBeenCalled() +}) diff --git a/src/shared/terminal-osc133-command-finished.ts b/src/shared/terminal-osc133-command-finished.ts index e4b9cdb8129..9468e1aaa5f 100644 --- a/src/shared/terminal-osc133-command-finished.ts +++ b/src/shared/terminal-osc133-command-finished.ts @@ -8,6 +8,8 @@ * terminators, best-effort exit codes) must be identical in both. */ +import { ownRetainedString } from './own-retained-string' + type OscTerminator = { index: number length: number @@ -91,6 +93,7 @@ export function createOsc133CommandFinishedScanner( if (carry.length > MAX_OSC_CARRY_LENGTH) { carry = carry.slice(carry.length - MAX_OSC_CARRY_LENGTH) } + carry = ownRetainedString(carry) return } diff --git a/src/shared/workspace-session-terminal-buffers.ts b/src/shared/workspace-session-terminal-buffers.ts index 706dbedc2d7..cf971d0d7ab 100644 --- a/src/shared/workspace-session-terminal-buffers.ts +++ b/src/shared/workspace-session-terminal-buffers.ts @@ -5,6 +5,7 @@ import { getRepoIdFromWorktreeId } from './worktree/id' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from './terminal-scrollback-limits' import { clampUtf8TextTail, isUtf8ByteLengthWithinLimit } from './utf8-byte-limits' import { parseExecutionHostId } from './execution-host' +import { ownRetainedString } from './own-retained-string' export type RepoConnection = Pick @@ -53,7 +54,9 @@ export function capTerminalScrollbackSessionBuffer(buffer: string): string { if (isUtf8ByteLengthWithinLimit(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT)) { return buffer } - return clampUtf8TextTail(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT).text + return ownRetainedString( + clampUtf8TextTail(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT).text + ) } function capTerminalScrollbackLeafBuffers(buffers: Record | undefined): { From 28c32f358736b6b01d28f0277203eef7e79f0b69 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 20:35:43 -0700 Subject: [PATCH 36/59] fix(stats): bound retained events during stalled writes (#20941) * fix(stats): cap retained events before asynchronous persistence * test: use typed access in memory retention regressions --------- Co-authored-by: m4air Co-authored-by: m4air --- src/main/stats/collector-async-save.test.ts | 40 +++++++++++++++++++++ src/main/stats/collector.ts | 4 +++ 2 files changed, 44 insertions(+) diff --git a/src/main/stats/collector-async-save.test.ts b/src/main/stats/collector-async-save.test.ts index 80d6c8b5497..5497c1d0d26 100644 --- a/src/main/stats/collector-async-save.test.ts +++ b/src/main/stats/collector-async-save.test.ts @@ -131,6 +131,46 @@ describe('StatsCollector async debounced save', () => { expect(JSON.parse(readFileSync(statsPath(), 'utf-8')).aggregates.totalAgentsSpawned).toBe(5) }) + it('bounds retained events while a stalled write prevents serialization', async () => { + vi.useFakeTimers() + const { StatsCollector, initStatsPath } = await importCollector() + initStatsPath() + const collector = new StatsCollector() + + gate.blocked = true + collector.record({ type: 'agent_start', at: 0 }) + await vi.advanceTimersByTimeAsync(5_000) + await vi.waitFor(() => expect(gate.writeFileCalls).toBe(1)) + + const expectedEvents = Array.from({ length: 10_000 }, (_, index) => ({ + type: 'agent_start', + at: index + 10_001 + })) + try { + for (let at = 1; at <= 20_000; at += 1) { + collector.record({ type: 'agent_start', at }) + if (at % 5_000 === 0) { + await vi.advanceTimersByTimeAsync(5_000) + expect(collector['events'].length).toBeLessThanOrEqual(10_000) + } + } + expect(gate.writeFileCalls).toBe(1) + expect(collector['events']).toEqual(expectedEvents) + } finally { + const flushed = collector.flushAsync() + gate.blocked = false + gate.waiters.splice(0).forEach((resolve) => resolve()) + await flushed + } + + const persisted = JSON.parse(readFileSync(statsPath(), 'utf-8')) + expect(persisted.events).toEqual(expectedEvents) + expect(persisted.aggregates).toMatchObject({ + totalAgentsSpawned: 20_001, + firstEventAt: 0 + }) + }) + it('retries a queued final snapshot after the active write fails', async () => { const { StatsCollector, initStatsPath } = await importCollector() initStatsPath() diff --git a/src/main/stats/collector.ts b/src/main/stats/collector.ts index ba484d63a57..9fd8d685c52 100644 --- a/src/main/stats/collector.ts +++ b/src/main/stats/collector.ts @@ -67,6 +67,10 @@ export class StatsCollector { record(event: StatsEvent): void { this.events.push(event) + // A stalled async write must not defer the in-memory retention limit. + if (this.events.length > MAX_EVENTS) { + this.events.splice(0, this.events.length - MAX_EVENTS) + } this.updateAggregates(event) this.scheduleSave() } From 691d9692e69fee60ffb4fb736babdbfde4a580b7 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:58:02 -0700 Subject: [PATCH 37/59] fix(pty): stop detached OMP tools on immediate terminal close (#20642) * test(omp): add opt-in owned PTY closure probe * fix(pty): sweep detached tools on immediate unrecognized shell close * test(omp): create close probe evidence root in fresh worktrees * test(pty): account for asynchronous immediate descendant cleanup * test(pty): reject inconclusive descendant cleanup probes --- .../daemon-audit-eligibility-event.test.ts | 1 + ...emon-authenticated-client-activity.test.ts | 1 + .../daemon/daemon-endpoint-ownership.test.ts | 1 + src/main/daemon/daemon-health.test.ts | 1 + src/main/daemon/daemon-idle-shutdown.test.ts | 1 + ...aemon-preflight-client-replacement.test.ts | 1 + ...-pty-adapter-cold-restore-reanchor.test.ts | 1 + ...emon-pty-adapter-cold-restore-seed.test.ts | 1 + ...on-pty-adapter-concurrent-recovery.test.ts | 1 + ...daemon-pty-adapter-daemon-recovery.test.ts | 1 + ...on-pty-adapter-history-checkpoints.test.ts | 1 + ...aemon-pty-adapter-history-recovery.test.ts | 1 + ...emon-pty-adapter-inventory-respawn.test.ts | 1 + ...pty-adapter-protocol-compatibility.test.ts | 1 + ...aemon-pty-adapter-session-adoption.test.ts | 1 + src/main/daemon/daemon-pty-adapter.test.ts | 1 + .../daemon-pty-router-history-handoff.test.ts | 1 + .../daemon-pty-upgrade-adoption.test.ts | 1 + ...emon-reattach-checkpoint-isolation.test.ts | 1 + .../daemon-restore-scrollback-depth.test.ts | 1 + .../daemon-self-retirement-respawn.test.ts | 1 + ...on-server-async-spawn-cancellation.test.ts | 1 + .../daemon/daemon-server-attach-only.test.ts | 1 + ...daemon-server-attachment-lifecycle.test.ts | 1 + .../daemon-server-error-handling.test.ts | 1 + .../daemon-server-kill-attribution.test.ts | 1 + src/main/daemon/daemon-server.test.ts | 1 + .../daemon-session-scrollback-window.test.ts | 1 + ...n-final-checkpoint-caller-deadline.test.ts | 1 + ...emon-stream-droppability-lifecycle.test.ts | 1 + ...aemon-transport-attachment-release.test.ts | 1 + ...6814-daemon-failure-classification.test.ts | 1 + src/main/daemon/mock-descendant-sweep.ts | 8 + src/main/daemon/reattach-snapshot.test.ts | 1 + .../slow-daemon-session-verification.test.ts | 1 + .../terminal-host-agent-session.test.ts | 1 + .../daemon/terminal-host-attach-only.test.ts | 1 + .../terminal-host-process-inspection.test.ts | 1 + .../terminal-host-readiness-reporting.test.ts | 1 + ...terminal-host-session-reaping-leak.test.ts | 16 +- src/main/daemon/terminal-host-startup.test.ts | 1 + .../daemon/terminal-host-wsl-context.test.ts | 1 + .../daemon/terminal-session-teardown.test.ts | 74 +++--- src/main/daemon/terminal-session-teardown.ts | 26 +- .../pty-listener-teardown-and-orphans.test.ts | 19 +- .../local-pty-provider-shutdown.test.ts | 9 +- src/main/providers/local-pty-termination.ts | 15 +- ...escendant-termination-job-coverage.test.ts | 3 +- ...session-host-authority.integration.test.ts | 1 + tests/tools/omp-close-lifecycle.md | 98 ++++++++ tests/tools/omp-close-lifecycle.test.mjs | 224 ++++++++++++++++++ 51 files changed, 459 insertions(+), 74 deletions(-) create mode 100644 src/main/daemon/mock-descendant-sweep.ts create mode 100644 tests/tools/omp-close-lifecycle.md create mode 100644 tests/tools/omp-close-lifecycle.test.mjs diff --git a/src/main/daemon/daemon-audit-eligibility-event.test.ts b/src/main/daemon/daemon-audit-eligibility-event.test.ts index 13d3e0f39ed..0b2c480118f 100644 --- a/src/main/daemon/daemon-audit-eligibility-event.test.ts +++ b/src/main/daemon/daemon-audit-eligibility-event.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/daemon/daemon-authenticated-client-activity.test.ts b/src/main/daemon/daemon-authenticated-client-activity.test.ts index 6579ced7a47..bc1e63ab8e2 100644 --- a/src/main/daemon/daemon-authenticated-client-activity.test.ts +++ b/src/main/daemon/daemon-authenticated-client-activity.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { readFileSync, mkdtempSync, rmSync } from 'node:fs' import { connect, type Socket } from 'node:net' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-endpoint-ownership.test.ts b/src/main/daemon/daemon-endpoint-ownership.test.ts index cf6af2e0956..7aff5fb32ee 100644 --- a/src/main/daemon/daemon-endpoint-ownership.test.ts +++ b/src/main/daemon/daemon-endpoint-ownership.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { existsSync, diff --git a/src/main/daemon/daemon-health.test.ts b/src/main/daemon/daemon-health.test.ts index 7d0d64271fa..c1b98c06397 100644 --- a/src/main/daemon/daemon-health.test.ts +++ b/src/main/daemon/daemon-health.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { spawn } from 'node:child_process' diff --git a/src/main/daemon/daemon-idle-shutdown.test.ts b/src/main/daemon/daemon-idle-shutdown.test.ts index 05f4b3aa39f..1500718e98f 100644 --- a/src/main/daemon/daemon-idle-shutdown.test.ts +++ b/src/main/daemon/daemon-idle-shutdown.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { EventEmitter } from 'node:events' import { connect, type Socket } from 'node:net' import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' diff --git a/src/main/daemon/daemon-preflight-client-replacement.test.ts b/src/main/daemon/daemon-preflight-client-replacement.test.ts index 7fbb46ebfd5..726c00a89ed 100644 --- a/src/main/daemon/daemon-preflight-client-replacement.test.ts +++ b/src/main/daemon/daemon-preflight-client-replacement.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { connect, type Socket } from 'node:net' import { join } from 'node:path' diff --git a/src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts b/src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts index fd3a7dca13c..b1b3d9a4259 100644 --- a/src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts +++ b/src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* Re-anchoring after a cold restore: aliveness probing, sticky restore cache, persistence. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { join } from 'node:path' diff --git a/src/main/daemon/daemon-pty-adapter-cold-restore-seed.test.ts b/src/main/daemon/daemon-pty-adapter-cold-restore-seed.test.ts index 71e56014a42..4b6c5e74b52 100644 --- a/src/main/daemon/daemon-pty-adapter-cold-restore-seed.test.ts +++ b/src/main/daemon/daemon-pty-adapter-cold-restore-seed.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* Cold-restore seed transfer and the payload shapes handed back to the renderer. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { hostname } from 'node:os' diff --git a/src/main/daemon/daemon-pty-adapter-concurrent-recovery.test.ts b/src/main/daemon/daemon-pty-adapter-concurrent-recovery.test.ts index 12805c0b3ee..66a3320ac81 100644 --- a/src/main/daemon/daemon-pty-adapter-concurrent-recovery.test.ts +++ b/src/main/daemon/daemon-pty-adapter-concurrent-recovery.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-pty-adapter-daemon-recovery.test.ts b/src/main/daemon/daemon-pty-adapter-daemon-recovery.test.ts index b53d75b2032..b6bb872c50b 100644 --- a/src/main/daemon/daemon-pty-adapter-daemon-recovery.test.ts +++ b/src/main/daemon/daemon-pty-adapter-daemon-recovery.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* Dead-endpoint write handling and daemon respawn after the daemon dies. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { existsSync, rmSync } from 'node:fs' diff --git a/src/main/daemon/daemon-pty-adapter-history-checkpoints.test.ts b/src/main/daemon/daemon-pty-adapter-history-checkpoints.test.ts index 3bdd53034d6..b0a5612c582 100644 --- a/src/main/daemon/daemon-pty-adapter-history-checkpoints.test.ts +++ b/src/main/daemon/daemon-pty-adapter-history-checkpoints.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* Periodic/final history checkpointing: scheduling, work caps, cooldown and shutdown writes. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { join } from 'node:path' diff --git a/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts b/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts index 4703f11300b..9e2914a6a56 100644 --- a/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts +++ b/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* History recovery / quarantine / reconcile regressions for DaemonPtyAdapter. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts b/src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts index f53e2c4c410..bccb0cbd19c 100644 --- a/src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts +++ b/src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* Inventory after the terminal host dies: worktree removal must not hard-fail. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { DaemonPtyAdapter } from './daemon-pty-adapter' diff --git a/src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts b/src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts index a2f2cbf3497..8132f2de4e2 100644 --- a/src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts +++ b/src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* DaemonPtyAdapter behaviour that varies with the negotiated daemon protocol version. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { rmSync } from 'node:fs' diff --git a/src/main/daemon/daemon-pty-adapter-session-adoption.test.ts b/src/main/daemon/daemon-pty-adapter-session-adoption.test.ts index cd6acc2352f..a603c21f037 100644 --- a/src/main/daemon/daemon-pty-adapter-session-adoption.test.ts +++ b/src/main/daemon/daemon-pty-adapter-session-adoption.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* Adopting daemon sessions that already exist: reattach, attach-only, inventory, tombstones, startup reconcile. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { join } from 'node:path' diff --git a/src/main/daemon/daemon-pty-adapter.test.ts b/src/main/daemon/daemon-pty-adapter.test.ts index 379d6a23b8f..410caddbe21 100644 --- a/src/main/daemon/daemon-pty-adapter.test.ts +++ b/src/main/daemon/daemon-pty-adapter.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /* Core IPtyProvider surface of DaemonPtyAdapter: spawn, io, sizing, teardown. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { rmSync } from 'node:fs' diff --git a/src/main/daemon/daemon-pty-router-history-handoff.test.ts b/src/main/daemon/daemon-pty-router-history-handoff.test.ts index e57375467ef..0a7eead083a 100644 --- a/src/main/daemon/daemon-pty-router-history-handoff.test.ts +++ b/src/main/daemon/daemon-pty-router-history-handoff.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-pty-upgrade-adoption.test.ts b/src/main/daemon/daemon-pty-upgrade-adoption.test.ts index 99c2a792e1e..7a92aecf150 100644 --- a/src/main/daemon/daemon-pty-upgrade-adoption.test.ts +++ b/src/main/daemon/daemon-pty-upgrade-adoption.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts b/src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts index b2c0bf59cbc..d716cb9372d 100644 --- a/src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts +++ b/src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-restore-scrollback-depth.test.ts b/src/main/daemon/daemon-restore-scrollback-depth.test.ts index b2c2c7c5028..dead1a7b64f 100644 --- a/src/main/daemon/daemon-restore-scrollback-depth.test.ts +++ b/src/main/daemon/daemon-restore-scrollback-depth.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-self-retirement-respawn.test.ts b/src/main/daemon/daemon-self-retirement-respawn.test.ts index 7d70d02e70a..7adaa3050fe 100644 --- a/src/main/daemon/daemon-self-retirement-respawn.test.ts +++ b/src/main/daemon/daemon-self-retirement-respawn.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { existsSync, mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/daemon/daemon-server-async-spawn-cancellation.test.ts b/src/main/daemon/daemon-server-async-spawn-cancellation.test.ts index 57585dc9318..7c42e88c93d 100644 --- a/src/main/daemon/daemon-server-async-spawn-cancellation.test.ts +++ b/src/main/daemon/daemon-server-async-spawn-cancellation.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/daemon/daemon-server-attach-only.test.ts b/src/main/daemon/daemon-server-attach-only.test.ts index 60e9ee83495..8b4099368a8 100644 --- a/src/main/daemon/daemon-server-attach-only.test.ts +++ b/src/main/daemon/daemon-server-attach-only.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-server-attachment-lifecycle.test.ts b/src/main/daemon/daemon-server-attachment-lifecycle.test.ts index 2f4d1b734ac..61243f56920 100644 --- a/src/main/daemon/daemon-server-attachment-lifecycle.test.ts +++ b/src/main/daemon/daemon-server-attachment-lifecycle.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { Socket } from 'node:net' import { mkdtempSync, rmSync } from 'node:fs' diff --git a/src/main/daemon/daemon-server-error-handling.test.ts b/src/main/daemon/daemon-server-error-handling.test.ts index 7ab9d1ffa95..adca3e19834 100644 --- a/src/main/daemon/daemon-server-error-handling.test.ts +++ b/src/main/daemon/daemon-server-error-handling.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { chmodSync, linkSync, mkdtempSync, readFileSync, rmSync, unlinkSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-server-kill-attribution.test.ts b/src/main/daemon/daemon-server-kill-attribution.test.ts index b2f506f42ed..d5c0bcf8bd2 100644 --- a/src/main/daemon/daemon-server-kill-attribution.test.ts +++ b/src/main/daemon/daemon-server-kill-attribution.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-server.test.ts b/src/main/daemon/daemon-server.test.ts index 27727aa2b3d..ef89b3ef3b0 100644 --- a/src/main/daemon/daemon-server.test.ts +++ b/src/main/daemon/daemon-server.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { connect, type Server, type Socket } from 'node:net' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-session-scrollback-window.test.ts b/src/main/daemon/daemon-session-scrollback-window.test.ts index b2dfb5118c9..d78ace1a038 100644 --- a/src/main/daemon/daemon-session-scrollback-window.test.ts +++ b/src/main/daemon/daemon-session-scrollback-window.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /** * OOM regression: a daemon owning 100+ terminals retained ~5000 rows of grid per session with no * bound, grew to ~1.9 GB, and was killed under system memory pressure — losing every session it diff --git a/src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts b/src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts index 64ae0f77ba0..f134a8c8f8c 100644 --- a/src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts +++ b/src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/daemon-stream-droppability-lifecycle.test.ts b/src/main/daemon/daemon-stream-droppability-lifecycle.test.ts index fda1fe25b98..e65e8ab18ba 100644 --- a/src/main/daemon/daemon-stream-droppability-lifecycle.test.ts +++ b/src/main/daemon/daemon-stream-droppability-lifecycle.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { randomUUID } from 'node:crypto' import type { Socket } from 'node:net' diff --git a/src/main/daemon/daemon-transport-attachment-release.test.ts b/src/main/daemon/daemon-transport-attachment-release.test.ts index 7b7c5e4fee6..511b9bf944e 100644 --- a/src/main/daemon/daemon-transport-attachment-release.test.ts +++ b/src/main/daemon/daemon-transport-attachment-release.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' /** * Attachment-leak regression: an attachment that outlives its transport leaves the session looking * viewed forever — producer pause/resume and any attachment-gated behavior then act on a client that diff --git a/src/main/daemon/issue-6814-daemon-failure-classification.test.ts b/src/main/daemon/issue-6814-daemon-failure-classification.test.ts index 35b84245a83..a7504a5e2a3 100644 --- a/src/main/daemon/issue-6814-daemon-failure-classification.test.ts +++ b/src/main/daemon/issue-6814-daemon-failure-classification.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' // Regression coverage for issue #6814 (terminal lockup after upgrade). // // Drives the real DaemonServer + checkDaemonHealth client over a real unix diff --git a/src/main/daemon/mock-descendant-sweep.ts b/src/main/daemon/mock-descendant-sweep.ts new file mode 100644 index 00000000000..e8baa228c10 --- /dev/null +++ b/src/main/daemon/mock-descendant-sweep.ts @@ -0,0 +1,8 @@ +import { vi } from 'vitest' + +// Mock subprocess PIDs must never reach the host process table or signal real descendants. +vi.mock('../pty-descendant-termination', () => ({ + killWithDescendantSweep: async (_pid: number, killRoot: () => void): Promise => { + killRoot() + } +})) diff --git a/src/main/daemon/reattach-snapshot.test.ts b/src/main/daemon/reattach-snapshot.test.ts index 9bbf7a103d6..c707521223e 100644 --- a/src/main/daemon/reattach-snapshot.test.ts +++ b/src/main/daemon/reattach-snapshot.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, describe, expect, it, vi } from 'vitest' import { TerminalHost } from './terminal-host' import { HeadlessEmulator } from './headless-emulator' diff --git a/src/main/daemon/slow-daemon-session-verification.test.ts b/src/main/daemon/slow-daemon-session-verification.test.ts index 082f1949a5f..9668f1de26d 100644 --- a/src/main/daemon/slow-daemon-session-verification.test.ts +++ b/src/main/daemon/slow-daemon-session-verification.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { connect, createServer, type Server, type Socket } from 'node:net' import { tmpdir } from 'node:os' diff --git a/src/main/daemon/terminal-host-agent-session.test.ts b/src/main/daemon/terminal-host-agent-session.test.ts index 1cf929fb93f..41b2f0c53e3 100644 --- a/src/main/daemon/terminal-host-agent-session.test.ts +++ b/src/main/daemon/terminal-host-agent-session.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SubprocessHandle } from './session-subprocess-handle' import { TerminalHost } from './terminal-host' diff --git a/src/main/daemon/terminal-host-attach-only.test.ts b/src/main/daemon/terminal-host-attach-only.test.ts index 97284451233..f33630536ee 100644 --- a/src/main/daemon/terminal-host-attach-only.test.ts +++ b/src/main/daemon/terminal-host-attach-only.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' import type { SubprocessHandle } from './session-subprocess-handle' import { TerminalHost, type TerminalHostOptions } from './terminal-host' diff --git a/src/main/daemon/terminal-host-process-inspection.test.ts b/src/main/daemon/terminal-host-process-inspection.test.ts index 50858a89ae1..eca0207dade 100644 --- a/src/main/daemon/terminal-host-process-inspection.test.ts +++ b/src/main/daemon/terminal-host-process-inspection.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi } from 'vitest' import type { SubprocessHandle } from './session-subprocess-handle' import { TerminalHost } from './terminal-host' diff --git a/src/main/daemon/terminal-host-readiness-reporting.test.ts b/src/main/daemon/terminal-host-readiness-reporting.test.ts index 677bcae258a..6e636e766e1 100644 --- a/src/main/daemon/terminal-host-readiness-reporting.test.ts +++ b/src/main/daemon/terminal-host-readiness-reporting.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SubprocessHandle } from './session-subprocess-handle' import { TerminalHost } from './terminal-host' diff --git a/src/main/daemon/terminal-host-session-reaping-leak.test.ts b/src/main/daemon/terminal-host-session-reaping-leak.test.ts index c937607a33a..4d0c53e1a77 100644 --- a/src/main/daemon/terminal-host-session-reaping-leak.test.ts +++ b/src/main/daemon/terminal-host-session-reaping-leak.test.ts @@ -130,13 +130,21 @@ describe('TerminalHost dead-session reaping (leak regression)', () => { }) lastSubprocess.forceKill = vi.fn() + let releaseSweep = (): void => {} + killWithDescendantSweepMock.mockImplementationOnce( + () => + new Promise((resolve) => { + releaseSweep = resolve + }) + ) const killed = host.kill('session-1', { immediate: true }) - // Immediate teardown skips the graceful kill and force-kills the child directly. On POSIX - // that reaches the child pgroup, so no Windows taskkill /T /F descendant sweep is needed. + expect(killWithDescendantSweepMock).toHaveBeenCalledTimes(1) expect(lastSubprocess.kill).not.toHaveBeenCalled() - expect(lastSubprocess.forceKill).toHaveBeenCalled() - expect(killWithDescendantSweepMock).not.toHaveBeenCalled() + expect(lastSubprocess.forceKill).not.toHaveBeenCalled() + expect(emulatorDispose).not.toHaveBeenCalled() + releaseSweep() + await vi.waitFor(() => expect(lastSubprocess.forceKill).toHaveBeenCalledTimes(1)) expect(emulatorDispose).not.toHaveBeenCalled() expect(host.listSessions()).toHaveLength(1) lastSubprocess._onExitCb?.(137) diff --git a/src/main/daemon/terminal-host-startup.test.ts b/src/main/daemon/terminal-host-startup.test.ts index e7b7c7b4c09..c6817b1e758 100644 --- a/src/main/daemon/terminal-host-startup.test.ts +++ b/src/main/daemon/terminal-host-startup.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TerminalHost } from './terminal-host' import type { SubprocessHandle } from './session-subprocess-handle' diff --git a/src/main/daemon/terminal-host-wsl-context.test.ts b/src/main/daemon/terminal-host-wsl-context.test.ts index 3958e370684..de0523b7ff1 100644 --- a/src/main/daemon/terminal-host-wsl-context.test.ts +++ b/src/main/daemon/terminal-host-wsl-context.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, describe, expect, it, vi } from 'vitest' import type * as WslModule from '../wsl' diff --git a/src/main/daemon/terminal-session-teardown.test.ts b/src/main/daemon/terminal-session-teardown.test.ts index 95010efd106..aeb9c7e31d2 100644 --- a/src/main/daemon/terminal-session-teardown.test.ts +++ b/src/main/daemon/terminal-session-teardown.test.ts @@ -61,50 +61,60 @@ describe('TerminalSessionTeardown plain-shell teardown', () => { expect(() => killRoot()).not.toThrow() }) - it('win32 immediate kill claims termination before awaiting the sweep', async () => { - // Why: createOrAttach rejects a doomed plain shell only via isTerminating, so the claim - // must land before the taskkill await or an attach can bind a pane to a dying session. - setPlatform('win32') - const session = createPlainShellSession() - const beginTermination = session.beginTermination as unknown as ReturnType - let claimedBeforeSweep = false - killWithDescendantSweepMock.mockImplementation(async () => { - claimedBeforeSweep = beginTermination.mock.calls.length === 1 - }) - const teardown = new TerminalSessionTeardown(new Map([['s1', session]])) + it.each(['win32', 'linux', 'darwin'] as const)( + '%s immediate kill claims termination before awaiting the sweep', + async (platform) => { + // Why: createOrAttach rejects a doomed plain shell only via isTerminating, so the claim + // must land before the taskkill await or an attach can bind a pane to a dying session. + setPlatform(platform) + const session = createPlainShellSession() + const beginTermination = session.beginTermination as unknown as ReturnType + let claimedBeforeSweep = false + killWithDescendantSweepMock.mockImplementation(async () => { + claimedBeforeSweep = beginTermination.mock.calls.length === 1 + }) + const teardown = new TerminalSessionTeardown(new Map([['s1', session]])) - await teardown.killSession('s1', session, true) + await teardown.killSession('s1', session, true) - expect(claimedBeforeSweep).toBe(true) - }) + expect(claimedBeforeSweep).toBe(true) + } + ) - it('win32 sweep ownsRoot guard requires the live session to still own the id', async () => { - setPlatform('win32') - const session = createPlainShellSession() - const sessions = new Map([['s1', session]]) - const teardown = new TerminalSessionTeardown(sessions) + it.each(['win32', 'linux', 'darwin'] as const)( + '%s sweep ownsRoot guard requires the live session to still own the id', + async (platform) => { + setPlatform(platform) + const session = createPlainShellSession() + const sessions = new Map([['s1', session]]) + const teardown = new TerminalSessionTeardown(sessions) - await teardown.killSession('s1', session, true) - const ownsRoot = (killWithDescendantSweepMock.mock.calls[0][2] as { ownsRoot: () => boolean }) - .ownsRoot - expect(ownsRoot()).toBe(true) + await teardown.killSession('s1', session, true) + const ownsRoot = (killWithDescendantSweepMock.mock.calls[0][2] as { ownsRoot: () => boolean }) + .ownsRoot + expect(ownsRoot()).toBe(true) - // A natural exit or reap must stop us from taskkilling a recycled PID. - ;(session as unknown as { isAlive: boolean }).isAlive = false - expect(ownsRoot()).toBe(false) - sessions.delete('s1') - ;(session as unknown as { isAlive: boolean }).isAlive = true - expect(ownsRoot()).toBe(false) - }) + // A natural exit or reap must stop us from taskkilling a recycled PID. + ;(session as unknown as { isAlive: boolean }).isAlive = false + expect(ownsRoot()).toBe(false) + sessions.delete('s1') + ;(session as unknown as { isAlive: boolean }).isAlive = true + expect(ownsRoot()).toBe(false) + } + ) - it('non-win32 immediate kill skips the tree kill (pgroup force-kill suffices)', async () => { + it('POSIX immediate close sweeps detached OMP tools before killing their parent', async () => { setPlatform('linux') const session = createPlainShellSession() const teardown = new TerminalSessionTeardown(new Map([['s1', session]])) await teardown.killSession('s1', session, true) - expect(killWithDescendantSweepMock).not.toHaveBeenCalled() + expect(killWithDescendantSweepMock).toHaveBeenCalledWith( + session.pid, + expect.any(Function), + expect.objectContaining({ ownsRoot: expect.any(Function) }) + ) expect(session.forceKillAndWaitForExit).toHaveBeenCalled() }) diff --git a/src/main/daemon/terminal-session-teardown.ts b/src/main/daemon/terminal-session-teardown.ts index 017f841a5e0..d35bfc4d64c 100644 --- a/src/main/daemon/terminal-session-teardown.ts +++ b/src/main/daemon/terminal-session-teardown.ts @@ -80,27 +80,13 @@ export class TerminalSessionTeardown { return operation } - /** - * Immediate teardown of a non-agent shell. On Windows, closing the ConPTY does not - * reap orphaned children (node-pty `useConptyDll` skips the console-process reap), so a - * live `pnpm i`/`node` survives shell exit, keeps the ConPTY console non-empty, and holds - * the worktree cwd — failing destructive worktree removal with "Failed to physically stop - * every PTY". Tree-kill only when the OS identity probe returns `own`; `unknown`/`foreign`/ - * `absent` skip taskkill and rely on root close alone. Mirrors the agent path - * (#10004/#10100). POSIX shells already reach their child pgroup on forceKill, so they - * stay on the plain force-kill path. - */ + /** Immediate close must reach detached tools even when startup did not identify an agent. */ private async forceKillPlainShellSession(sessionId: string, session: Session): Promise { - if (process.platform === 'win32') { - // Why: forceKillAndWaitForExit claims termination synchronously; awaiting the sweep - // ahead of it would leave attach open on a doomed session for the taskkill's duration. - session.beginTermination() - await killWithDescendantSweep(session.pid, () => {}, { - // Why: the descendant tree is only ours while this Session still owns the live root PID. - ownsRoot: () => this.sessions.get(sessionId) === session && session.isAlive, - terminateOwnedTree: () => session.terminateOwnedTree() - }) - } + session.beginTermination() + await killWithDescendantSweep(session.pid, () => {}, { + ownsRoot: () => this.sessions.get(sessionId) === session && session.isAlive, + terminateOwnedTree: () => session.terminateOwnedTree() + }) await session.forceKillAndWaitForExit() } diff --git a/src/main/ipc/pty-listener-teardown-and-orphans.test.ts b/src/main/ipc/pty-listener-teardown-and-orphans.test.ts index ae749f8dbe0..7e4178f35d3 100644 --- a/src/main/ipc/pty-listener-teardown-and-orphans.test.ts +++ b/src/main/ipc/pty-listener-teardown-and-orphans.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { existsSyncMock, + loginPreflightExecFileMock, spawnMock, openCodeClearPtyMock, piClearPtyMock @@ -162,9 +163,25 @@ describe('registerPtyHandlers', () => { rows: 24 })) as { id: string } + let finishSnapshot: (() => void) | undefined + loginPreflightExecFileMock.mockImplementationOnce( + ( + _file: string, + _args: string[], + _options: unknown, + callback: (error: Error | null, stdout: string) => void + ) => { + finishSnapshot = () => callback(null, '') + } + ) const killPromise = handlers.get('pty:kill')!(null, { id: spawnResult.id }) as Promise - expect(killSpy).toHaveBeenCalledTimes(1) + await vi.waitFor(() => expect(finishSnapshot).toBeTypeOf('function')) + expect(killSpy).not.toHaveBeenCalled() + expect(onDataDisposable.dispose).not.toHaveBeenCalled() + expect(onExitDisposable.dispose).not.toHaveBeenCalled() + finishSnapshot?.() + await vi.waitFor(() => expect(killSpy).toHaveBeenCalledTimes(1)) expect(onDataDisposable.dispose).not.toHaveBeenCalled() expect(onExitDisposable.dispose).not.toHaveBeenCalled() diff --git a/src/main/providers/local-pty-provider-shutdown.test.ts b/src/main/providers/local-pty-provider-shutdown.test.ts index cb7595800c1..cccede7186b 100644 --- a/src/main/providers/local-pty-provider-shutdown.test.ts +++ b/src/main/providers/local-pty-provider-shutdown.test.ts @@ -513,13 +513,16 @@ describe('LocalPtyProvider', () => { expect(killWithDescendantSweepMock).not.toHaveBeenCalled() }) - it('non-win32 immediate shutdown of a plain shell skips the tree kill', async () => { - // beforeEach pins platform to linux; POSIX force-kill already reaches the child pgroup. + it('POSIX immediate shutdown sweeps detached OMP tools without startup recognition', async () => { const { id } = await provider.spawn({ cols: 80, rows: 24 }) await provider.shutdown(id, { immediate: true }) - expect(killWithDescendantSweepMock).not.toHaveBeenCalled() + expect(killWithDescendantSweepMock).toHaveBeenCalledWith( + mockProc.pid, + expect.any(Function), + expect.objectContaining({ ownsRoot: expect.any(Function) }) + ) }) }) diff --git a/src/main/providers/local-pty-termination.ts b/src/main/providers/local-pty-termination.ts index 470d5b600ab..7a19e9d7326 100644 --- a/src/main/providers/local-pty-termination.ts +++ b/src/main/providers/local-pty-termination.ts @@ -175,19 +175,8 @@ async function shutdownTrackedPty( operation.rootSignalled = true requestTrackedPtyShutdown(id, proc, operation.immediate) } - if (ptyAgentSessionIds.has(id)) { - // Why: POSIX needs a pre-kill descendant snapshot; Windows tree-kills only when the - // identity probe returns `own` so agent/MCP orphans cannot hold the worktree cwd - // (#10004). `unknown`/`foreign`/`absent` skip taskkill and rely on root close alone. - await killWithDescendantSweep(proc.pid, signalRoot, { - ownsRoot: () => ptyProcesses.get(id) === proc, - terminateOwnedTree: () => terminatePtyJob(proc) - }) - } else if (process.platform === 'win32' && operation.immediate) { - // Why: a plain shell's ConPTY teardown doesn't reap orphaned children (useConptyDll - // skips the console reap), so a live `pnpm i`/`node` keeps the ConPTY console alive and - // holds the worktree cwd. Tree kill runs only when the OS identity probe returns `own`; - // otherwise root close alone, and detached children may block physical stop (#10004). + if (ptyAgentSessionIds.has(id) || operation.immediate) { + // Typed agents also detach tool process groups; immediate close must snapshot before root exit. await killWithDescendantSweep(proc.pid, signalRoot, { ownsRoot: () => ptyProcesses.get(id) === proc, terminateOwnedTree: () => terminatePtyJob(proc) diff --git a/src/main/pty-descendant-termination-job-coverage.test.ts b/src/main/pty-descendant-termination-job-coverage.test.ts index 21e14012caa..738d16ab16f 100644 --- a/src/main/pty-descendant-termination-job-coverage.test.ts +++ b/src/main/pty-descendant-termination-job-coverage.test.ts @@ -16,7 +16,8 @@ import { describe, expect, it } from 'vitest' */ const SRC_DIR = join(__dirname, '..') const CALL = 'killWithDescendantSweep(' -const EXPECTED_MINIMUM_SITES = 5 +// Local immediate and recognized-agent shutdown share one guarded call site. +const EXPECTED_MINIMUM_SITES = 4 function collectTypeScriptFiles(dir: string): string[] { const found: string[] = [] diff --git a/src/main/runtime/remote-agent-session-host-authority.integration.test.ts b/src/main/runtime/remote-agent-session-host-authority.integration.test.ts index 56d74744238..884a80a36fb 100644 --- a/src/main/runtime/remote-agent-session-host-authority.integration.test.ts +++ b/src/main/runtime/remote-agent-session-host-authority.integration.test.ts @@ -1,3 +1,4 @@ +import '../daemon/mock-descendant-sweep' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/tests/tools/omp-close-lifecycle.md b/tests/tools/omp-close-lifecycle.md new file mode 100644 index 00000000000..03e2dd6fc31 --- /dev/null +++ b/tests/tools/omp-close-lifecycle.md @@ -0,0 +1,98 @@ +# OMP owned-PTY close probe (#9530) + +This opt-in probe launches an actual installed OMP binary in disposable local PTYs +and calls Orca's production `shutdownLocalPty` and `killAllLocalPtys` functions, +or daemon `Session`, native subprocess handle, and `TerminalSessionTeardown`. +It sets the same agent-session ownership flag that `activateLocalPtySession` sets +for `launchAgent` / recognized startup commands, then repeats without that flag +to represent OMP typed into a shell. This isolates termination policy; it does not +exercise Agent button delivery or terminal-tab/handle routing. + +```sh +ORCA_BACKGROUND_LAUNCH=1 ORCA_OMP_PROBE_BINARY=/absolute/path/to/omp \ + node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts \ + tests/tools/omp-close-lifecycle.test.mjs +``` + +The probe defaults to zsh on macOS and bash on other POSIX hosts. Set +`ORCA_OMP_PROBE_SHELL` to the absolute path of either shell to override. Windows +is skipped. It requires the existing node-pty native dependency for the current +Node runtime. The normal unit suite skips the test unless a binary is supplied. + +Each case waits five seconds for OMP startup, captures the owned process tree, +requests explicit close or local quit cleanup, and verifies those exact process +IDs are absent using host `ps` after a six-second observation window. It records +raw terminal output and before/after process rows in `.bench-fixtures/omp-close-*`. +The fixture contains no prompt or model request. It disables the first-run setup +wizard, startup splash and update checks in a temporary config; OMP's normal tools +and extensions remain enabled. HOME, ZDOTDIR, XDG_CONFIG_HOME and OMP's agent home +are disposable. Cleanup signals only owned identities with matching process start +time and group, then removes the temporary home. + +## Observed on 2026-09-14 + +At Orca base `93c370246388`, macOS arm64, installed `omp/18.1.18`: + +- Explicit local close with the agent flag: shell and foreground OMP exited. +- Explicit local close without the flag: shell and foreground OMP exited. +- Local quit cleanup with or without the flag: shell and foreground OMP exited. +- Explicit close used the existing five-second force deadline for the shell. + Quit removes native exit tracking immediately, so the probe uses independent + host process evidence; an empty provider map is not its exit oracle. + +The same four outcomes were observed in an initial first-run setup-splash pass. +The normal-idle transcript displayed the OMP prompt and reported no LSP servers. +No stale foreground OMP was reproduced in these local termination-policy cases. + +## Detached external tool reproduction and correction + +Set `ORCA_OMP_PROBE_EXTERNAL_TOOL=1` to run `! /bin/sleep 120` in OMP before +explicit immediate close. Add `ORCA_OMP_PROBE_BACKEND=daemon` to exercise the daemon +backend. Each mode tests both recognized and typed launches; these modes do not +run the local-quit cases. The probe makes no model requests. Both OMP/PI profiles +are cleared, and XDG data/cache/state roots are isolated alongside configuration. + +On macOS, installed Orca `1.4.202-hourly.202609132311` and OMP `18.1.18`, an actual +non-focus CLI-created terminal reproduced the detached-child leak: shell PID +71632 and OMP PID 71667 exited after CLI close, but sleep PID 72125 (PGID 72125) +remained after the grace window, reparented to PID 1. The owned survivor was +cleaned using its captured PID/start-time/group identity. This is a detached-tool +leak, not a reproduction of the reported foreground OMP surviving for days. + +With the correction, all four actual OMP/external-sleep cases (recognized/typed, +local/daemon) left none of the captured shell, OMP or sleep PIDs present. This +runs production backend code with real PTYs; it does not run a rebuilt installed +app through the CLI. Reports/transcripts remain local under `.bench-fixtures/`. + +### Termination contract + +Immediate close now uses the existing descendant sweep for all local-provider +and daemon shells, including agents typed after startup. This also terminates +still-parented, intentionally detached jobs that previously survived POSIX close. +The sweep captures descendants before root exit, checks current root ownership, +and retains the existing identity-guarded delayed escalation. It adds a bounded +process-table capture (one-second timeout) and, when descendants exist, the +existing single two-second delayed recheck; there is no recurring polling. +Daemon termination is claimed before awaiting capture, preventing reattachment. +Physical root exit still gates session reaping. Snapshot failure falls back to +root termination; children already reparented before capture are not covered. + +The execution host runs this policy. Paired runtimes using these backends receive +the fix when their host updates; no wire fields or client-side remote PID signals +are added. Direct SSH relay PTYs use separate `src/relay/pty-handler.ts` termination +and are not fixed or runtime-validated by this change. Graceful plain-shell +shutdown, disconnect and daemon/remote keep-alive policy are unchanged. The code +uses no repository metadata and applies to folder workspaces as well as worktrees. +Windows retains its existing guarded job/tree termination; this probe skips it. + +## Limits and next evidence + +Do not close #9530 from this probe. The original report did not identify Orca/OMP +versions or the exact close action. A tab can disappear without this termination +entry point running, which this probe does not cover. It also does not exercise +full app quit lifecycle, background/floating/mobile handle resolution, a busy +model turn, initialized eval workers or LSPs, Windows/WSL/Linux execution, or live SSH ownership. Daemon/remote keep-alive is intentional and remains unchanged. + +A failing reproduction needs the original surface/close action, provider mode, +owning runtime, and process identities before and after. Signal-resistant fixture +processes alone do not establish that current OMP has the reported leak. diff --git a/tests/tools/omp-close-lifecycle.test.mjs b/tests/tools/omp-close-lifecycle.test.mjs new file mode 100644 index 00000000000..47df8325c56 --- /dev/null +++ b/tests/tools/omp-close-lifecycle.test.mjs @@ -0,0 +1,224 @@ +import { it, expect } from 'vitest' +import * as pty from 'node-pty' +import { Session } from '../../src/main/daemon/session.ts' +import { TerminalSessionTeardown } from '../../src/main/daemon/terminal-session-teardown.ts' +import { createDaemonPtySubprocessHandle } from '../../src/main/daemon/pty-subprocess/subprocess-handle.ts' +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { runProcess } from '../../src/shared/child-process/run-process.ts' +import { + captureDescendantSnapshot, + readProcessTable +} from '../../src/main/pty-descendant-termination.ts' +import { + createPtyPhysicalExit, + shutdownLocalPty, + killAllLocalPtys +} from '../../src/main/providers/local-pty-termination.ts' +import { + ptyProcesses, + ptyAgentSessionIds, + ptyPhysicalExits, + ptyExitDisposables, + clearPtyState +} from '../../src/main/providers/local-pty-provider-state.ts' + +const binary = process.env.ORCA_OMP_PROBE_BINARY +const externalTool = process.env.ORCA_OMP_PROBE_EXTERNAL_TOOL === '1' +const daemonBackend = process.env.ORCA_OMP_PROBE_BACKEND === 'daemon' +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) +const quote = (value) => `'${value.replaceAll("'", "'\\''")}'` +const ownedPidRows = async (pids) => { + const result = await runProcess({ + program: 'ps', + args: ['-p', pids.join(','), '-o', 'pid=,ppid=,pgid=,stat=,comm='], + maxOutputBytes: 16000 + }) + expect(result.timedOut).toBe(false) + expect(result.signal).toBeNull() + expect(result.stderr.trim()).toBe('') + expect([0, 1]).toContain(result.code) + if (result.code === 1) { + expect(result.stdout.trim()).toBe('') + } + return result.stdout.trim() +} +it.skipIf(!binary || process.platform === 'win32')( + 'observes actual OMP under production owned-PTY closure policy', + async () => { + const fixtures = join(process.cwd(), '.bench-fixtures') + mkdirSync(fixtures, { recursive: true }) + const output = mkdtempSync(join(fixtures, 'omp-close-')) + const report = [] + for (const launch of ['recognized', 'typed']) { + for (const close of externalTool || daemonBackend ? ['explicit'] : ['explicit', 'quit']) { + expect(ptyProcesses.size).toBe(0) + const home = mkdtempSync(join(tmpdir(), 'orca-omp-close-home-')) + const agentHome = join(home, 'agent') + mkdirSync(agentHome) + const config = join(home, 'probe.yml') + writeFileSync( + config, + 'startup:\n setupWizard: false\n showSplash: false\n checkUpdate: false\n' + ) + const id = `${launch}-${close}` + let transcript = '' + let nativeExit = null + const shell = + process.env.ORCA_OMP_PROBE_SHELL ?? + (process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash') + const shellArgs = shell.endsWith('zsh') ? ['-f', '-i'] : ['--noprofile', '--norc', '-i'] + const proc = pty.spawn(shell, shellArgs, { + name: 'xterm-256color', + cols: 120, + rows: 35, + cwd: home, + env: { + ...process.env, + HOME: home, + USERPROFILE: home, + ZDOTDIR: home, + XDG_CONFIG_HOME: join(home, 'config'), + XDG_DATA_HOME: join(home, 'data'), + XDG_CACHE_HOME: join(home, 'cache'), + XDG_STATE_HOME: join(home, 'state'), + OMP_CODING_AGENT_DIR: agentHome, + PI_CODING_AGENT_DIR: agentHome, + OMP_PROFILE: '', + PI_PROFILE: '', + PI_CONFIG_DIR: '.omp', + PI_CONFIG_FILES: '', + ORCA_BACKGROUND_LAUNCH: '1' + } + }) + const daemonSession = daemonBackend + ? new Session({ + sessionId: id, + cols: 120, + rows: 35, + shellReadySupported: false, + ...(launch === 'recognized' ? { launchAgent: 'omp' } : {}), + subprocess: createDaemonPtySubprocessHandle({ + process: proc, + shellPath: shell, + spawnCwd: home, + env: process.env, + startupCommandDeliveredInShellArgs: false, + reportsChildExitStatus: true, + sessionId: id, + startupAgentRecognition: null + }) + }) + : null + proc.onData((data) => { + transcript = (transcript + data).slice(-131072) + }) + if (!daemonSession) { + ptyProcesses.set(id, proc) + createPtyPhysicalExit(id) + if (launch === 'recognized') { + ptyAgentSessionIds.add(id) + } + } + ptyExitDisposables.set( + id, + proc.onExit((event) => { + nativeExit = event + ptyPhysicalExits.get(id)?.markExited() + clearPtyState(id) + rmSync(home, { recursive: true, force: true }) + }) + ) + let snapshot + try { + proc.write(`${quote(binary)} --no-session --config ${quote(config)}\r`) + await delay(5000) + snapshot = await captureDescendantSnapshot(proc.pid) + expect(snapshot?.descendants.length).toBeGreaterThan(0) + if (externalTool) { + proc.write('! /bin/sleep 120\r') + for (let attempt = 0; attempt < 25; attempt++) { + await delay(200) + snapshot = await captureDescendantSnapshot(proc.pid) + if (snapshot?.descendants.length > 1) { + break + } + } + expect(snapshot?.descendants.length).toBeGreaterThan(1) + } + const pids = [proc.pid, ...snapshot.descendants.map((row) => row.pid)] + const before = await ownedPidRows(pids) + expect(before).toContain('omp') + if (externalTool) { + expect(before).toContain('sleep') + } + const started = Date.now() + let closeError = null + try { + if (daemonSession) { + await new TerminalSessionTeardown(new Map([[id, daemonSession]])).killSession( + id, + daemonSession, + true + ) + } else if (close === 'explicit') { + await shutdownLocalPty(id, { immediate: externalTool }) + } else { + killAllLocalPtys() + } + } catch (error) { + closeError = String(error) + } + await delay(6000) + const after = await ownedPidRows(pids) + report.push({ + launch, + close, + externalTool, + backend: daemonBackend ? 'daemon' : 'local', + before, + after, + nativeExit, + tracked: daemonSession ? daemonSession.isAlive : ptyProcesses.has(id), + elapsedMs: Date.now() - started, + closeError, + home + }) + writeFileSync(join(output, `${id}.txt`), transcript) + writeFileSync(join(output, 'report.json'), JSON.stringify(report, null, 2)) + expect(closeError).toBeNull() + expect(after).toBe('') + } finally { + if (snapshot) { + const current = await readProcessTable() + const owned = [ + ...snapshot.descendants, + ...(snapshot.root ? [{ ...snapshot.root, pgid: snapshot.rootPgid }] : []) + ] + for (const row of current.rows) { + if ( + owned.some( + (known) => + known.pid === row.pid && + known.startedAt === row.startedAt && + known.pgid === row.pgid + ) + ) { + try { + process.kill(row.pid, 'SIGKILL') + } catch {} + } + } + } + daemonSession?.dispose() + clearPtyState(id) + rmSync(home, { recursive: true, force: true }) + } + } + } + writeFileSync(join(output, 'report.json'), JSON.stringify(report, null, 2)) + console.log(output) + }, + 90000 +) From 8c6ae79e94a8c86b3ed05b4dd46f2f45ae0d5d33 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:58:27 -0700 Subject: [PATCH 38/59] fix(relay): stop detached tools on immediate terminal close (#20645) * fix(relay): sweep detached tools on immediate terminal close * test(relay): reject failed process cleanup queries --- src/relay/mock-descendant-sweep.ts | 8 + src/relay/pty-handler-attach-replay.test.ts | 1 + .../pty-handler-dispose-lifecycle.test.ts | 1 + src/relay/pty-handler-grace-timer.test.ts | 1 + .../pty-handler-immediate-descendants.test.ts | 282 ++++++++++++++++++ ...handler-inventory-process-evidence.test.ts | 1 + ...-handler-output-drain-differential.test.ts | 1 + .../pty-handler-output-streaming.test.ts | 1 + .../pty-handler-ownership-attestation.test.ts | 1 + .../pty-handler-resize-stale-pty.test.ts | 1 + .../pty-handler-retired-pane-surface.test.ts | 1 + src/relay/pty-handler-revive.test.ts | 1 + .../pty-handler-shell-resolution.test.ts | 1 + .../pty-handler-shutdown-signals.test.ts | 1 + .../pty-handler-source-publication.test.ts | 1 + src/relay/pty-handler-spawn-admission.test.ts | 1 + src/relay/pty-handler-spawn-cwd.test.ts | 1 + .../pty-handler-spawn-environment.test.ts | 1 + ...y-handler-startup-command-delivery.test.ts | 1 + ...ler-windows-child-process-evidence.test.ts | 1 + src/relay/pty-handler.ts | 52 +++- src/relay/relay-daemon-fatal-reap.test.ts | 1 + tests/tools/omp-relay-close-lifecycle.md | 67 +++++ .../tools/omp-relay-close-lifecycle.test.mjs | 150 ++++++++++ 24 files changed, 575 insertions(+), 3 deletions(-) create mode 100644 src/relay/mock-descendant-sweep.ts create mode 100644 src/relay/pty-handler-immediate-descendants.test.ts create mode 100644 tests/tools/omp-relay-close-lifecycle.md create mode 100644 tests/tools/omp-relay-close-lifecycle.test.mjs diff --git a/src/relay/mock-descendant-sweep.ts b/src/relay/mock-descendant-sweep.ts new file mode 100644 index 00000000000..8af046417f9 --- /dev/null +++ b/src/relay/mock-descendant-sweep.ts @@ -0,0 +1,8 @@ +import { vi } from 'vitest' + +// Mock PTYs reuse the runner PID; never enumerate or signal its real descendants. +vi.mock('../main/pty-descendant-termination', () => ({ + killWithDescendantSweep: async (_pid: number, killRoot: () => void): Promise => { + killRoot() + } +})) diff --git a/src/relay/pty-handler-attach-replay.test.ts b/src/relay/pty-handler-attach-replay.test.ts index e289547bf0c..5ec55a6eb2a 100644 --- a/src/relay/pty-handler-attach-replay.test.ts +++ b/src/relay/pty-handler-attach-replay.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import * as ptyShellUtils from './pty-shell-utils' import { diff --git a/src/relay/pty-handler-dispose-lifecycle.test.ts b/src/relay/pty-handler-dispose-lifecycle.test.ts index 0553f92a4e5..631834ab1b7 100644 --- a/src/relay/pty-handler-dispose-lifecycle.test.ts +++ b/src/relay/pty-handler-dispose-lifecycle.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' const { mockPtySpawn, mockPtyInstance, mockCreateShellPromptReadinessProbe } = vi.hoisted(() => ({ diff --git a/src/relay/pty-handler-grace-timer.test.ts b/src/relay/pty-handler-grace-timer.test.ts index c1797583c14..ed552e71551 100644 --- a/src/relay/pty-handler-grace-timer.test.ts +++ b/src/relay/pty-handler-grace-timer.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { DEFAULT_BOUNDED_SSH_RELAY_GRACE_PERIOD_SECONDS } from '../shared/ssh-types' diff --git a/src/relay/pty-handler-immediate-descendants.test.ts b/src/relay/pty-handler-immediate-descendants.test.ts new file mode 100644 index 00000000000..1362052834a --- /dev/null +++ b/src/relay/pty-handler-immediate-descendants.test.ts @@ -0,0 +1,282 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { beginPtyHandlerTest, endPtyHandlerTest } from './pty-handler-test-harness' +import type { MockDispatcher } from './pty-handler-test-harness' +import type { PtyHandler } from './pty-handler' +import type { RelayPtySourcePublication } from './relay-pty-source-publication' + +const { mockPtySpawn, mockPtyInstance, mockCreateShellPromptReadinessProbe, sweep } = vi.hoisted( + () => ({ + mockPtySpawn: vi.fn(), + mockCreateShellPromptReadinessProbe: vi.fn(), + sweep: + vi.fn< + (pid: number, killRoot: () => void, deps?: { ownsRoot?: () => boolean }) => Promise + >(), + mockPtyInstance: { + pid: process.pid, + onData: vi.fn(), + onExit: vi.fn(), + write: vi.fn(), + resize: vi.fn(), + kill: vi.fn(), + clear: vi.fn(), + pause: vi.fn(), + resume: vi.fn() + } + }) +) +vi.mock('node-pty', () => ({ spawn: mockPtySpawn })) +vi.mock('../main/pty-descendant-termination', () => ({ killWithDescendantSweep: sweep })) +vi.mock('../main/pty/posix-pty-process-groups', () => ({ + forceKillPosixPtyProcessGroups: (_pid: number, kill: () => void) => kill() +})) +vi.mock('../main/shell-prompt-readiness-probe', () => ({ + createShellPromptReadinessProbe: mockCreateShellPromptReadinessProbe +})) + +const ensure = { + claim: { + digestVersion: 1, + keyId: 'claim-key', + identityDigest: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + worktreeScopeDigest: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + agent: 'omp' + }, + surface: { + worktreeId: 'repo::/tmp/worktree', + tabId: '11111111-1111-4111-8111-111111111111', + leafId: '22222222-2222-4222-8222-222222222222', + terminalHandle: 'term_omp' + } +} + +describe('relay immediate descendant cleanup', () => { + let dispatcher: MockDispatcher + let handler: PtyHandler + let originalPlatform: PropertyDescriptor | undefined + let exit: ((event: { exitCode: number }) => void) | undefined + let release: (() => void) | undefined + let kill: ReturnType + + beforeEach(() => { + ;({ dispatcher, handler, originalPlatform } = beginPtyHandlerTest({ + mockPtySpawn, + mockPtyInstance, + mockCreateShellPromptReadinessProbe + })) + exit = undefined + release = undefined + kill = vi.fn() + mockPtySpawn.mockReturnValue({ + ...mockPtyInstance, + kill, + onExit: (callback: (event: { exitCode: number }) => void) => { + exit = callback + } + }) + sweep.mockReset() + sweep.mockImplementation( + (_pid, killRoot) => + new Promise((resolve, reject) => { + release = () => { + try { + killRoot() + resolve() + } catch (error) { + reject(error) + } + } + }) + ) + }) + afterEach(async () => { + release?.() + exit?.({ exitCode: 137 }) + await endPtyHandlerTest(handler, originalPlatform) + }) + + async function spawn(params: Record = {}) { + const result = await dispatcher.callRequest('pty.spawn', params) + if ( + !result || + typeof result !== 'object' || + !('id' in result) || + typeof result.id !== 'string' + ) { + throw new Error('missing PTY id') + } + return result.id + } + const close = (id: string) => dispatcher.callRequest('pty.shutdown', { id, immediate: true }) + + it('sweeps a typed agent before force-kill and joins close through physical exit', async () => { + const id = await spawn() + const first = close(id) + const second = close(id) + expect(sweep).toHaveBeenCalledTimes(1) + expect(kill).not.toHaveBeenCalled() + await expect(dispatcher.callRequest('pty.attach', { id })).rejects.toThrow('terminating') + release?.() + await vi.waitFor(() => expect(kill).toHaveBeenCalledWith('SIGKILL')) + expect(handler.activePtyCount).toBe(1) + exit?.({ exitCode: 137 }) + await Promise.all([first, second]) + expect(handler.activePtyCount).toBe(0) + expect(kill).toHaveBeenCalledTimes(1) + }) + + it('does not signal a root that exits while its snapshot is pending', async () => { + const id = await spawn() + const closing = close(id) + const ownsRoot = sweep.mock.calls[0]?.[2]?.ownsRoot + expect(ownsRoot?.()).toBe(true) + exit?.({ exitCode: 0 }) + expect(ownsRoot?.()).toBe(false) + release?.() + await closing + expect(kill).not.toHaveBeenCalled() + }) + + it('retains the agent claim instead of adopting or duplicating a closing owner', async () => { + const id = await spawn({ agentSessionEnsure: ensure }) + const closing = close(id) + await expect(spawn({ agentSessionEnsure: ensure })).rejects.toThrow('terminating') + expect(mockPtySpawn).toHaveBeenCalledTimes(1) + release?.() + await vi.waitFor(() => expect(kill).toHaveBeenCalledTimes(1)) + exit?.({ exitCode: 137 }) + await closing + }) + + it('does not replay a completed create operation while its owner is closing', async () => { + const params = { + agentSessionEnsure: ensure, + agentSessionCreateOperationId: 'ccccccccccccccccccccccccccccccccccccccccccc' + } + const id = await spawn(params) + const closing = close(id) + await expect(spawn(params)).rejects.toThrow('terminating') + expect(mockPtySpawn).toHaveBeenCalledTimes(1) + release?.() + await vi.waitFor(() => expect(kill).toHaveBeenCalledTimes(1)) + exit?.({ exitCode: 137 }) + await closing + }) + + it('allows retry after a failed root signal without releasing the live PTY', async () => { + const id = await spawn() + kill.mockImplementationOnce(() => { + throw new Error('signal refused') + }) + const rejected = expect(close(id)).rejects.toThrow('signal refused') + release?.() + await rejected + expect(handler.activePtyCount).toBe(1) + const retry = close(id) + expect(sweep).toHaveBeenCalledTimes(2) + release?.() + await vi.waitFor(() => expect(kill).toHaveBeenCalledTimes(2)) + exit?.({ exitCode: 137 }) + await retry + }) + + it('keeps the Windows force-kill path and fences attachment until physical exit', async () => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + const id = await spawn() + const closing = close(id) + expect(sweep).not.toHaveBeenCalled() + expect(kill).toHaveBeenCalledWith() + await expect(dispatcher.callRequest('pty.attach', { id })).rejects.toThrow('terminating') + exit?.({ exitCode: 137 }) + await closing + }) + + it('keeps graceful shell shutdown off the descendant sweep', async () => { + const id = await spawn() + await dispatcher.callRequest('pty.shutdown', { id, immediate: false }) + expect(sweep).not.toHaveBeenCalled() + expect(kill).toHaveBeenCalledWith('SIGTERM') + }) + it('refuses attach after close completes during source checkpoint wait', async () => { + const id = await spawn() + let finishSource!: (ready: boolean) => void + const sourceWait = new Promise((resolve) => { + finishSource = resolve + }) + const activate = vi.fn(() => false) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This stub supplies every publication method exercised by the handler. + handler.setSourcePublication({ + accepts: () => false, + exitPublicationSettled: () => true, + sealAndPublishExit: () => false, + publish: () => false, + onCreditAvailable: () => {}, + receivingActivation: () => undefined, + waitForPendingSend: () => sourceWait, + activate, + getDebugSnapshot: () => ({}), + dispose: () => {} + } as unknown as RelayPtySourcePublication) + const attaching = dispatcher.callRequest('pty.attach', { + id, + sourceRecovery: { + status: 'checkpoint', + deliveryToken: 'token', + ptyIncarnation: 'incarnation', + clientGeneration: 1, + ownerGeneration: 1, + acceptedSourceEndSu: 0 + } + }) + const closing = close(id) + release?.() + await vi.waitFor(() => expect(kill).toHaveBeenCalledTimes(1)) + exit?.({ exitCode: 137 }) + await closing + expect(handler.activePtyCount).toBe(0) + finishSource(true) + await expect(attaching).rejects.toThrow() + expect(activate).not.toHaveBeenCalled() + }) + + it('retains claim if close starts before initial claim liveness validation', async () => { + let closing: Promise | undefined + let closeId = '' + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This stub supplies every publication method exercised by the handler. + handler.setSourcePublication({ + accepts: () => false, + exitPublicationSettled: () => true, + sealAndPublishExit: () => false, + publish: () => false, + onCreditAvailable: () => {}, + receivingActivation: () => undefined, + waitForPendingSend: async () => true, + activate: (id: string) => { + if (!closeId) { + closeId = id + queueMicrotask(() => { + closing = close(id) + void closing.catch(() => {}) + }) + } + return false + }, + getDebugSnapshot: () => ({}), + dispose: () => {} + } as unknown as RelayPtySourcePublication) + await expect(spawn({ agentSessionEnsure: ensure })).rejects.toThrow('terminating') + expect(handler.activePtyCount).toBe(1) + const firstExit = exit + const retried = spawn({ agentSessionEnsure: ensure }) + const outcome = await retried.then( + () => 'created', + () => 'rejected' + ) + const spawnCount = mockPtySpawn.mock.calls.length + release?.() + firstExit?.({ exitCode: 137 }) + await closing + expect(outcome).toBe('rejected') + expect(spawnCount).toBe(1) + }) +}) diff --git a/src/relay/pty-handler-inventory-process-evidence.test.ts b/src/relay/pty-handler-inventory-process-evidence.test.ts index f0d5b068304..0ab15977f0b 100644 --- a/src/relay/pty-handler-inventory-process-evidence.test.ts +++ b/src/relay/pty-handler-inventory-process-evidence.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' // Regression guard for the SHIPPED inventory path. `pty.listProcesses` resolves // every managed pane's title from one batched host capture; a per-pane tree walk // would restore the O(panes x rows) scan on the relay's single event-loop thread, diff --git a/src/relay/pty-handler-output-drain-differential.test.ts b/src/relay/pty-handler-output-drain-differential.test.ts index cbaee4bcb75..d6c7e92470f 100644 --- a/src/relay/pty-handler-output-drain-differential.test.ts +++ b/src/relay/pty-handler-output-drain-differential.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' const { mockPtySpawn, mockPtyInstance } = vi.hoisted(() => ({ diff --git a/src/relay/pty-handler-output-streaming.test.ts b/src/relay/pty-handler-output-streaming.test.ts index 3be79255fcc..af0a4cbdfaa 100644 --- a/src/relay/pty-handler-output-streaming.test.ts +++ b/src/relay/pty-handler-output-streaming.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { PTY_STARTUP_INGRESS_VERSION } from '../shared/pty-startup-ingress' diff --git a/src/relay/pty-handler-ownership-attestation.test.ts b/src/relay/pty-handler-ownership-attestation.test.ts index ee1c144df09..513917ef76c 100644 --- a/src/relay/pty-handler-ownership-attestation.test.ts +++ b/src/relay/pty-handler-ownership-attestation.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' // The host half of #9819: a client may only reap a relay PTY it can prove it created, so the relay // has to say who created each one. The attestation is read from the live consumer grant, never from // a spawn parameter — otherwise it would just echo the caller's claim back at it. diff --git a/src/relay/pty-handler-resize-stale-pty.test.ts b/src/relay/pty-handler-resize-stale-pty.test.ts index 0dcb6b56497..d3478bc6825 100644 --- a/src/relay/pty-handler-resize-stale-pty.test.ts +++ b/src/relay/pty-handler-resize-stale-pty.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { mockPtySpawn, mockPtyInstance, mockCreateShellPromptReadinessProbe } = vi.hoisted(() => ({ diff --git a/src/relay/pty-handler-retired-pane-surface.test.ts b/src/relay/pty-handler-retired-pane-surface.test.ts index 959c6f06330..c8f50ebf13a 100644 --- a/src/relay/pty-handler-retired-pane-surface.test.ts +++ b/src/relay/pty-handler-retired-pane-surface.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' const { mockPtySpawn, mockPtyInstance, mockCreateShellPromptReadinessProbe } = vi.hoisted(() => ({ diff --git a/src/relay/pty-handler-revive.test.ts b/src/relay/pty-handler-revive.test.ts index bd0dffbd1b8..88155e38242 100644 --- a/src/relay/pty-handler-revive.test.ts +++ b/src/relay/pty-handler-revive.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { existsSync, rmSync } from 'node:fs' import { homedir, tmpdir } from 'node:os' diff --git a/src/relay/pty-handler-shell-resolution.test.ts b/src/relay/pty-handler-shell-resolution.test.ts index 9abd9b90d10..679c2babab7 100644 --- a/src/relay/pty-handler-shell-resolution.test.ts +++ b/src/relay/pty-handler-shell-resolution.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import * as gitBash from '../main/git-bash' import * as ptyShellUtils from './pty-shell-utils' diff --git a/src/relay/pty-handler-shutdown-signals.test.ts b/src/relay/pty-handler-shutdown-signals.test.ts index 951ce7209e0..5bc3373599c 100644 --- a/src/relay/pty-handler-shutdown-signals.test.ts +++ b/src/relay/pty-handler-shutdown-signals.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' const { mockPtySpawn, mockPtyInstance, mockCreateShellPromptReadinessProbe } = vi.hoisted(() => ({ diff --git a/src/relay/pty-handler-source-publication.test.ts b/src/relay/pty-handler-source-publication.test.ts index 71e8c12b48f..341931cffb5 100644 --- a/src/relay/pty-handler-source-publication.test.ts +++ b/src/relay/pty-handler-source-publication.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { PTY_STARTUP_INGRESS_VERSION } from '../shared/pty-startup-ingress' import { diff --git a/src/relay/pty-handler-spawn-admission.test.ts b/src/relay/pty-handler-spawn-admission.test.ts index 6fef02a9cc0..ea5c6ca3486 100644 --- a/src/relay/pty-handler-spawn-admission.test.ts +++ b/src/relay/pty-handler-spawn-admission.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/relay/pty-handler-spawn-cwd.test.ts b/src/relay/pty-handler-spawn-cwd.test.ts index 2aab95a401e..fd2c64849e4 100644 --- a/src/relay/pty-handler-spawn-cwd.test.ts +++ b/src/relay/pty-handler-spawn-cwd.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { mkdtempSync, mkdirSync, rmSync } from 'node:fs' import { homedir, tmpdir } from 'node:os' diff --git a/src/relay/pty-handler-spawn-environment.test.ts b/src/relay/pty-handler-spawn-environment.test.ts index 8879686821e..020b1256cc3 100644 --- a/src/relay/pty-handler-spawn-environment.test.ts +++ b/src/relay/pty-handler-spawn-environment.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { homedir, tmpdir } from 'node:os' diff --git a/src/relay/pty-handler-startup-command-delivery.test.ts b/src/relay/pty-handler-startup-command-delivery.test.ts index 9e7c29202c7..2215eff9d76 100644 --- a/src/relay/pty-handler-startup-command-delivery.test.ts +++ b/src/relay/pty-handler-startup-command-delivery.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/relay/pty-handler-windows-child-process-evidence.test.ts b/src/relay/pty-handler-windows-child-process-evidence.test.ts index 6d723824a04..75f7382df8d 100644 --- a/src/relay/pty-handler-windows-child-process-evidence.test.ts +++ b/src/relay/pty-handler-windows-child-process-evidence.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' // Regression guard for the Windows SSH child-process answer. The relay used to return a hardcoded // `false` here, which every close guard reads as "nothing is running in this pane" -- so a Windows // SSH pane running a build closed with no prompt. The answer now comes from the process table, and diff --git a/src/relay/pty-handler.ts b/src/relay/pty-handler.ts index 4a55d6b587b..80b0bd62620 100644 --- a/src/relay/pty-handler.ts +++ b/src/relay/pty-handler.ts @@ -1,5 +1,6 @@ /* oxlint-disable max-lines */ import type { IPty } from 'node-pty' +import { killWithDescendantSweep } from '../main/pty-descendant-termination' import type * as NodePty from 'node-pty' import { existsSync } from 'node:fs' import { basename, join } from 'node:path' @@ -236,6 +237,7 @@ type ManagedPty = { * spawn reply to skip waiting for a marker that will never come (fish, sh, Windows). */ shellReadyArmed?: boolean physicalExit?: PhysicalExitTracker + immediateClose?: Promise forceKillSent?: boolean gracefulKillSent?: boolean startupIngress?: PtyStartupIngress @@ -1675,6 +1677,7 @@ export class PtyHandler { const existing = this.agentSessionCreateOperations.get(operationId) if (existing) { const result = await existing + this.assertPtyNotClosing(this.ptys.get(result.id)) this.sourcePublication?.activate(result.id, result.incarnationId, context) const sourceActivation = context && this.sourcePublication?.receivingActivation?.(result.id, context.clientId) @@ -1789,6 +1792,7 @@ export class PtyHandler { this.agentSessionOwners.release(result.owner.ptyId, result.owner.generation) throw new Error('agent_session_exited_during_start') } + this.assertPtyNotClosing(managed) managed.agentSessionOwners = this.agentSessionOwners.listForPty(managed.id) const adoptedReplay = result.disposition === 'adopted' ? managed.buffered.read() : '' this.sourcePublication?.activate(managed.id, managed.incarnationId, context) @@ -2060,6 +2064,8 @@ export class PtyHandler { throw new Error(`PTY "${id}" not found`) } + this.assertPtyNotClosing(managed) + // Why: verify liveness because shells can exit without node-pty onExit. if (this.reapPtyProvenExited(managed)) { // Why the marker: this is the ONLY not-found answer backed by a liveness check. The unmarked @@ -2098,6 +2104,10 @@ export class PtyHandler { ) { sourceRecovery = Object.freeze({ status: 'checkpointUnavailable' }) } + if (this.ptys.get(id) !== managed || managed.disposed) { + throw new Error(`PTY "${id}" not found`) + } + this.assertPtyNotClosing(managed) const activation = this.sourcePublication?.activate( id, managed.incarnationId, @@ -2272,15 +2282,51 @@ export class PtyHandler { if (immediate) { this.releaseStartupCommand(managed) this.flushPtyOutput(id) - this.requestForceKill(managed) - // Why: preserve timed-out entries so onExit/retry owns native handles. - await this.waitForPhysicalExit(managed, IMMEDIATE_PTY_EXIT_TIMEOUT_MS) + await this.closeImmediately(managed) } else { this.releaseStartupCommand(managed) this.requestGracefulKill(managed, 'force-kill') } } + private assertPtyNotClosing(managed: ManagedPty | undefined): void { + if (managed?.immediateClose) { + throw new Error(`PTY "${managed.id}" is terminating`) + } + } + + private async closeImmediately(managed: ManagedPty): Promise { + if (managed.immediateClose) { + return managed.immediateClose + } + const ownsRoot = (): boolean => this.ptys.get(managed.id) === managed && !managed.disposed + const close = async (): Promise => { + if (process.platform === 'win32') { + this.requestForceKill(managed) + } else { + await killWithDescendantSweep( + managed.pty.pid, + () => { + if (ownsRoot()) { + this.requestForceKill(managed) + } + }, + { ownsRoot, terminateOwnedTree: () => terminatePtyJob(managed.pty) } + ) + } + await this.waitForPhysicalExit(managed, IMMEDIATE_PTY_EXIT_TIMEOUT_MS) + } + const pending = close() + managed.immediateClose = pending + try { + await pending + } finally { + if (managed.immediateClose === pending) { + managed.immediateClose = undefined + } + } + } + /** Re-decide, on the host, whether the caller may destroy this PTY. * * `pty.shutdown` is irreversible and its siblings `pty.spawn`/`pty.attach` already take a diff --git a/src/relay/relay-daemon-fatal-reap.test.ts b/src/relay/relay-daemon-fatal-reap.test.ts index 071185fc311..9e94af4c78c 100644 --- a/src/relay/relay-daemon-fatal-reap.test.ts +++ b/src/relay/relay-daemon-fatal-reap.test.ts @@ -1,3 +1,4 @@ +import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { PtyHandler } from './pty-handler' diff --git a/tests/tools/omp-relay-close-lifecycle.md b/tests/tools/omp-relay-close-lifecycle.md new file mode 100644 index 00000000000..d0ef5052ba3 --- /dev/null +++ b/tests/tools/omp-relay-close-lifecycle.md @@ -0,0 +1,67 @@ +# OMP relay-host immediate-close probe (#9530) + +This opt-in probe uses a real installed OMP binary and native PTYs behind production +`PtyHandler` spawn/data/shutdown handlers. The dispatcher is an in-process test +transport; no SSH connection or rendered client is exercised. OMP source is read-only. + +```sh +ORCA_BACKGROUND_LAUNCH=1 ORCA_OMP_PROBE_BINARY=/absolute/path/to/omp \ + ORCA_OMP_PROBE_SHELL=/bin/bash \ + node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts \ + tests/tools/omp-relay-close-lifecycle.test.mjs +``` + +The test defaults to zsh on macOS and bash on Linux. Windows is skipped. It needs +existing native node-pty dependencies; do not install or rebuild as part of the probe. +HOME, user profile, XDG roots and OMP/PI agent roots are disposable, profiles cleared, +and zsh inheritance fenced to the disposable root. No model request is made. The +probe runs `! /bin/sleep 120`, records exact shell/OMP/tool process rows, requests +immediate close, and observes those PIDs independently of the relay inventory. +Matching PID/start-time/group identities bound leftover cleanup. Reports and capped +terminal transcripts stay in `.bench-fixtures/omp-relay-close-*`. + +## Measured on macOS with OMP 18.1.18 + +At source base `93c370246388`, bash mode leaves sleep PID 43156, PGID 43156, alive +and reparented to PID 1 after root PID 42902 and OMP PID 42949 exit. The zsh control +exits cleanly: OMP uses a headless PTY for zsh/fish user-shell tools, while bash +uses its embedded-shell subprocess path. Thus an external command alone does not +determine the process lifetime; the configured user shell matters. + +With the correction, the same bash probe leaves none of its captured PIDs present. +This is detached-tool leakage, not proof of the original foreground-OMP-survives +report. The local-provider/daemon correction is PR #20642; this probe and correction +cover the separate direct-relay backend. + +## Reliability contract + +- Invariant: `terminal-session.explicit-close-retirement`. Explicit immediate close + captures still-parented detached descendants before root termination, preserves + the exact host owner through physical exit, and cannot attach/adopt that owner + while the close is pending. A concurrent close joins the same operation. +- Failure source/oracle: actual OMP external sleep survives the bash-mode relay + close before the fix; independently queried owned PIDs are absent afterward. + Unit tests also cover pending attachment/adoption/create replay, natural exit + during capture, signal failure/retry, retained claims during initial promotion, + and close completing while attachment awaits a source checkpoint. +- Gate: the existing experimental explicit-close gate's descendant/backend tests, + relay lifecycle suites and this opt-in real-PTY probe. Live SSH transport and + rendered client flows remain explicit validation gaps. +- Budget: one existing bounded process-table capture (one-second timeout, 32-MiB + cap), plus one bounded identity recheck after the two-second grace when there + are descendants. Same-turn captures coalesce; no recurring polling is added. +- Authority: the execution host does all process inspection/signaling. Pending-close + refusal carries no proven-exited marker; it is not evidence of process death. + No new RPC fields/opcodes or required capabilities. Older clients receive an + ordinary failed attach while close is pending, not a successful doomed attachment. +- Scope: every immediate POSIX relay close, including still-parented intentionally + detached jobs. Graceful close, disconnect grace, keep-alive and fatal-exit/dispose + policies are unchanged. Windows retains its immediate force-kill path and now + rejects attachment during the physical-exit wait. Folder workspaces and worktrees + use the same PTY identity, without repository metadata checks. +- Gaps: macOS runtime evidence only; Linux/Windows/WSL runtime, live SSH/mobile and + mixed-version clients are not exercised. Children reparented before capture and + same-second identity ambiguity retain the incumbent cleanup limitations. + +Mock-PTY suites isolate the sweep: their fake PIDs often equal the test runner's PID +and must never reach the real host process table or descendant signals. diff --git a/tests/tools/omp-relay-close-lifecycle.test.mjs b/tests/tools/omp-relay-close-lifecycle.test.mjs new file mode 100644 index 00000000000..2730356ebd1 --- /dev/null +++ b/tests/tools/omp-relay-close-lifecycle.test.mjs @@ -0,0 +1,150 @@ +import { it, expect } from 'vitest' +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { + createMockDispatcher, + createTestPtyHandler +} from '../../src/relay/pty-handler-test-harness.ts' +import { + captureDescendantSnapshot, + readProcessTable +} from '../../src/main/pty-descendant-termination.ts' +import { runProcess } from '../../src/shared/child-process/run-process.ts' + +const binary = process.env.ORCA_OMP_PROBE_BINARY +const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) +const quote = (value) => `'${value.replaceAll("'", "'\\''")}'` + +it.skipIf(!binary || process.platform === 'win32')( + 'closes actual OMP detached tools through the relay host', + async () => { + const fixtures = join(process.cwd(), '.bench-fixtures') + mkdirSync(fixtures, { recursive: true }) + const output = mkdtempSync(join(fixtures, 'omp-relay-close-')) + const home = mkdtempSync(join(tmpdir(), 'orca-omp-relay-close-home-')) + const agentHome = join(home, 'agent') + mkdirSync(agentHome) + const config = join(home, 'probe.yml') + writeFileSync( + config, + 'startup:\n setupWizard: false\n showSplash: false\n checkUpdate: false\n' + ) + const dispatcher = createMockDispatcher() + let transcript = '' + dispatcher.notify = (method, params) => { + if (method === 'pty.data' && typeof params?.data === 'string') { + transcript = (transcript + params.data).slice(-131072) + } + } + const handler = createTestPtyHandler(dispatcher) + let snapshot + let id + try { + const spawned = await dispatcher.callRequest('pty.spawn', { + cwd: home, + cols: 120, + rows: 35, + env: { + HOME: home, + USERPROFILE: home, + ZDOTDIR: home, + ORCA_ORIG_ZDOTDIR: home, + SHELL: + process.env.ORCA_OMP_PROBE_SHELL ?? + (process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash'), + XDG_CONFIG_HOME: join(home, 'config'), + XDG_DATA_HOME: join(home, 'data'), + XDG_CACHE_HOME: join(home, 'cache'), + XDG_STATE_HOME: join(home, 'state'), + OMP_CODING_AGENT_DIR: agentHome, + PI_CODING_AGENT_DIR: agentHome, + OMP_PROFILE: '', + PI_PROFILE: '', + PI_CONFIG_DIR: '.omp', + PI_CONFIG_FILES: '', + ORCA_BACKGROUND_LAUNCH: '1', + ORCA_PANE_KEY: 'omp-relay-probe:owned-leaf', + ORCA_TAB_ID: 'omp-relay-probe' + }, + envToDelete: ['BASH_ENV', 'ENV', 'ORCA_OMP_STATUS_EXTENSION', 'ORCA_PI_STATUS_EXTENSION'] + }) + id = spawned.id + const [entry] = JSON.parse(await dispatcher.callRequest('pty.serialize', { ids: [id] })) + snapshot = await captureDescendantSnapshot(entry.pid) + expect(snapshot?.root?.pid).toBe(entry.pid) + dispatcher.callNotification('pty.data', { + id, + data: `${quote(binary)} --no-session --config ${quote(config)}\r` + }) + await pause(5000) + dispatcher.callNotification('pty.data', { id, data: '! /bin/sleep 120\r' }) + for (let attempt = 0; attempt < 25; attempt++) { + await pause(200) + snapshot = await captureDescendantSnapshot(entry.pid) + if (snapshot?.descendants.length > 1) { + break + } + } + expect(snapshot?.descendants.length).toBeGreaterThan(1) + const pids = [entry.pid, ...snapshot.descendants.map((row) => row.pid)] + const rows = async () => { + const result = await runProcess({ + program: 'ps', + args: ['-p', pids.join(','), '-o', 'pid=,ppid=,pgid=,stat=,comm='], + maxOutputBytes: 16000 + }) + expect(result.timedOut).toBe(false) + expect(result.signal).toBeNull() + expect(result.stderr.trim()).toBe('') + expect([0, 1]).toContain(result.code) + if (result.code === 1) { + expect(result.stdout.trim()).toBe('') + } + return result.stdout.trim() + } + const before = await rows() + expect(before).toContain('omp') + expect(before).toContain('sleep') + await dispatcher.callRequest('pty.shutdown', { + id, + immediate: true, + expectedIncarnationId: spawned.incarnationId + }) + await pause(6000) + const after = await rows() + writeFileSync( + join(output, 'report.json'), + JSON.stringify({ backend: 'relay-host', before, after, pid: entry.pid, id, home }, null, 2) + ) + writeFileSync(join(output, 'transcript.txt'), transcript) + console.log(output) + expect(after).toBe('') + } finally { + if (snapshot) { + const current = await readProcessTable() + const owned = [ + ...snapshot.descendants, + ...(snapshot.root ? [{ ...snapshot.root, pgid: snapshot.rootPgid }] : []) + ] + for (const row of current.rows) { + if ( + owned.some( + (known) => + known.pid === row.pid && + known.startedAt === row.startedAt && + known.pgid === row.pgid + ) + ) { + try { + process.kill(row.pid, 'SIGKILL') + } catch {} + } + } + } + await handler.dispose({ waitForPhysicalExit: false }) + rmSync(home, { recursive: true, force: true }) + } + }, + 45000 +) From 3de77340fc99de53aeb980bbd3c8cb01917bf87d Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:11:33 -0400 Subject: [PATCH 39/59] fix: apply managed Claude auth to Agent Teams (#21356) * fix: apply managed Claude auth to agent teams * test: update agent teams auth launch expectation * refactor: derive agent teams auth deletions --- src/cli/handlers/core.test.ts | 46 +++++++++++++++++++ src/cli/handlers/core.ts | 20 ++++---- ...index-worktree-selector-resolution.test.ts | 1 + ...resolve-terminal-split-source-authority.ts | 20 ++++++-- src/main/runtime/orca-runtime-state-fields.ts | 5 ++ .../terminal/terminal-lifecycle-methods.ts | 3 +- .../startup/main-process-runtime-service.ts | 1 + .../rpc-contract/terminal-unary-params.ts | 3 +- 8 files changed, 86 insertions(+), 13 deletions(-) diff --git a/src/cli/handlers/core.test.ts b/src/cli/handlers/core.test.ts index ac5444566d9..ba2daf90c30 100644 --- a/src/cli/handlers/core.test.ts +++ b/src/cli/handlers/core.test.ts @@ -130,4 +130,50 @@ describe('orca claude-teams CLI handler', () => { expect(spawnEnv.PATH).toBe('/shim:/usr/bin') } ) + + it.skipIf(isWindows)('removes managed auth variables before spawning Claude', async () => { + const previousApiKey = process.env.ANTHROPIC_API_KEY + process.env.ANTHROPIC_API_KEY = 'sk-ant-inherited' + callMock.mockResolvedValueOnce({ + result: { + launch: { + env: { + CLAUDE_CONFIG_DIR: '/managed/claude', + CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' + }, + envToDelete: ['ANTHROPIC_API_KEY'] + } + } + }) + try { + await runClaudeTeams() + } finally { + if (previousApiKey === undefined) { + delete process.env.ANTHROPIC_API_KEY + } else { + process.env.ANTHROPIC_API_KEY = previousApiKey + } + } + + const spawnEnv = spawnMock.mock.calls.at(-1)?.[2].env as SpawnEnv + expect(spawnEnv.ANTHROPIC_API_KEY).toBeUndefined() + expect(spawnEnv.CLAUDE_CONFIG_DIR).toBe('/managed/claude') + }) + + it.skipIf(isWindows)('preserves API-key auth when no managed deletion is requested', async () => { + const previousApiKey = process.env.ANTHROPIC_API_KEY + process.env.ANTHROPIC_API_KEY = 'sk-ant-system' + try { + await runClaudeTeams() + } finally { + if (previousApiKey === undefined) { + delete process.env.ANTHROPIC_API_KEY + } else { + process.env.ANTHROPIC_API_KEY = previousApiKey + } + } + + const spawnEnv = spawnMock.mock.calls.at(-1)?.[2].env as SpawnEnv + expect(spawnEnv.ANTHROPIC_API_KEY).toBe('sk-ant-system') + }) }) diff --git a/src/cli/handlers/core.ts b/src/cli/handlers/core.ts index d4979ff2ae9..6a1b7ab3918 100644 --- a/src/cli/handlers/core.ts +++ b/src/cli/handlers/core.ts @@ -73,16 +73,20 @@ export const CORE_HANDLERS: Record = { 'orca claude-teams must be run inside an Orca terminal.' ) } - const response = await client.call<{ launch: { env: Record } }>( - 'agentTeams.prepareLaunch', - { - paneKey, - env: envRecord() - } - ) + const inheritedEnv = envRecord() + const response = await client.call<{ + launch: { env: Record; envToDelete?: string[] } + }>('agentTeams.prepareLaunch', { + paneKey, + env: inheritedEnv, + prepareAuth: true + }) + for (const key of response.result.launch.envToDelete ?? []) { + delete inheritedEnv[key] + } process.exitCode = await runClaudeAgentTeams( { - ...envRecord(), + ...inheritedEnv, ...response.result.launch.env }, rawArgs ?? [] diff --git a/src/cli/index-worktree-selector-resolution.test.ts b/src/cli/index-worktree-selector-resolution.test.ts index 70c5ca57ad5..c3f3d7dbd60 100644 --- a/src/cli/index-worktree-selector-resolution.test.ts +++ b/src/cli/index-worktree-selector-resolution.test.ts @@ -153,6 +153,7 @@ describe('orca cli worktree awareness', () => { expect(callMock).toHaveBeenCalledWith('agentTeams.prepareLaunch', { paneKey: 'tab-1:11111111-1111-4111-8111-111111111111', + prepareAuth: true, env: expect.objectContaining({ ORCA_PANE_KEY: 'tab-1:11111111-1111-4111-8111-111111111111' }) diff --git a/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts b/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts index 4723e186039..49d55d96c17 100644 --- a/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts +++ b/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts @@ -14,6 +14,7 @@ import { ensureClaudeAgentTeamsShimDir, resolveClaudeAgentTeamsShimBin } from './claude-agent-teams-shim-env' +import { applyClaudeEnvPatch } from '../claude-accounts/environment' export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRuntimeWithSplitPtyBackedTerminal { protected resolveTerminalSplitSourceAuthority( @@ -109,6 +110,7 @@ export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRunt async prepareClaudeAgentTeamsLeader(args: { paneKey: string baseEnv?: Record + prepareAuth?: boolean }): Promise<{ env: Record }> { const handle = this.getTerminalHandleForPaneKey(args.paneKey) if (!handle) { @@ -116,26 +118,38 @@ export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRunt } return await this.prepareClaudeAgentTeamsLeaderForHandle({ handle, - baseEnv: args.baseEnv + baseEnv: args.baseEnv, + prepareAuth: args.prepareAuth }) } async prepareClaudeAgentTeamsLeaderForHandle(args: { handle: string baseEnv?: Record - }): Promise<{ env: Record }> { + prepareAuth?: boolean + }): Promise<{ env: Record; envToDelete?: string[] }> { const baseEnv = { ...process.env, ...args.baseEnv } + const inheritedEnvKeys = new Set(Object.keys(baseEnv)) + const auth = args.prepareAuth && this.prepareClaudeAuth ? await this.prepareClaudeAuth() : null + if (auth) { + applyClaudeEnvPatch(baseEnv, auth.envPatch, { stripAuthEnv: auth.stripAuthEnv }) + } + const envToDelete = auth?.stripAuthEnv + ? [...inheritedEnvKeys].filter((key) => !(key in baseEnv)) + : undefined const shimDir = await ensureClaudeAgentTeamsShimDir() const shimBin = resolveClaudeAgentTeamsShimBin(baseEnv) - return this.claudeAgentTeams.createLaunchEnv({ + const launch = this.claudeAgentTeams.createLaunchEnv({ leaderHandle: args.handle, baseEnv, shimDir, shimBin }) + const env = auth ? { ...auth.envPatch, ...launch.env } : launch.env + return envToDelete ? { env, envToDelete } : { env } } // Why: a leader handle that never binds to a PTY (lost pane race) has no exit diff --git a/src/main/runtime/orca-runtime-state-fields.ts b/src/main/runtime/orca-runtime-state-fields.ts index 781f6075be1..a28ead15724 100644 --- a/src/main/runtime/orca-runtime-state-fields.ts +++ b/src/main/runtime/orca-runtime-state-fields.ts @@ -3,6 +3,7 @@ import { OrcaRuntimeWithLinearCommands } from './orca-runtime-linear-commands' import type { RuntimeStore } from './runtime-store-contract' import type { StatsCollector } from '../stats/collector' import type { IPtyProvider } from '../providers/types' +import type { PrepareClaudeAuth } from '../ipc/pty/host-env/types' import type { RuntimeTerminalAgentStatusEvent } from './runtime-terminal-contracts' import type { TerminalSideEffectBatch } from '../../shared/terminal-side-effect-facts' import type { AgentStatusIpcPayload } from '../../shared/agent-status-types' @@ -41,12 +42,15 @@ import { registerConptyDa1OverrideInstaller } from './terminal-model-query-autho import { registerTerminalViewAttributesApplier } from './terminal-view-attribute-store' export class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCommands { + protected readonly prepareClaudeAuth?: PrepareClaudeAuth + constructor( store: RuntimeStore | null = null, stats?: StatsCollector, deps?: { getLocalProvider?: () => IPtyProvider getSshProvider?: (connectionId: string) => IPtyProvider | undefined + prepareClaudeAuth?: PrepareClaudeAuth onPtyStopped?: (ptyId: string) => void onTerminalAgentStatus?: (event: RuntimeTerminalAgentStatusEvent) => void onTerminalSideEffects?: (batch: TerminalSideEffectBatch) => void @@ -103,6 +107,7 @@ export class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCommands { ) { super() this.store = store + this.prepareClaudeAuth = deps?.prepareClaudeAuth store?.onSettingsChanged?.((updates) => { if ('experimentalStructuredNativeChat' in updates) { this.notifyMobileSessionTabsChanged() diff --git a/src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts b/src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts index 2fcdc2bc92c..58e1d5d825c 100644 --- a/src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts +++ b/src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts @@ -189,7 +189,8 @@ export const TERMINAL_LIFECYCLE_METHODS = [ handler: async (params, { runtime }) => ({ launch: await runtime.prepareClaudeAgentTeamsLeader({ paneKey: params.paneKey, - baseEnv: params.env + baseEnv: params.env, + prepareAuth: params.prepareAuth }) }) }) diff --git a/src/main/startup/main-process-runtime-service.ts b/src/main/startup/main-process-runtime-service.ts index 62716bbcdec..35d008d1b40 100644 --- a/src/main/startup/main-process-runtime-service.ts +++ b/src/main/startup/main-process-runtime-service.ts @@ -72,6 +72,7 @@ export function initializeMainProcessRuntime(): OrcaRuntimeService { // `orca serve`, which never opens one, and the fleet path runs there too. const observedPaneIdentities = new AgentStatusObservedPaneIdentities() const runtime = new OrcaRuntimeService(store, stats, { + prepareClaudeAuth: (target) => state.claudeRuntimeAuth!.prepareForClaudeLaunch(target), agentSessionClaimSigner: loadAgentSessionClaimSigner( getProfileUserDataPath(), getProfileUserDataPath() diff --git a/src/shared/rpc-contract/terminal-unary-params.ts b/src/shared/rpc-contract/terminal-unary-params.ts index 9b735952096..ae34af7ebd9 100644 --- a/src/shared/rpc-contract/terminal-unary-params.ts +++ b/src/shared/rpc-contract/terminal-unary-params.ts @@ -234,5 +234,6 @@ export const AgentTeamsTmuxCompat = z.object({ export const AgentTeamsPrepareLaunch = z.object({ paneKey: requiredString('Missing pane key'), - env: z.record(z.string(), z.string()).optional() + env: z.record(z.string(), z.string()).optional(), + prepareAuth: z.boolean().optional() }) From a84f16df3d794a5a55d579566715600084798cc1 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:12:36 -0400 Subject: [PATCH 40/59] fix(mobile): mint one pairing offer per Continue on the sidebar page (#21261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mobile): mint one pairing offer per Continue on the sidebar page Step 2 auto-minted as soon as it became visible, which is the same commit that starts the network-interface lookup. The offer therefore advertised whatever address was left over from the last visit (or none at all, so main picked its own default), and when the lookup settled on a different address the refresh handler reminted with rotate: true. Two overlapping getPairingQR calls then raced for one pending credential: main rotates the pending device away for the rotate mint, and orders concurrent offers by arrival at its generation counter rather than by the order the renderer issued them, so the request the pane is waiting on can be the one main decided to supersede. Defer the auto-mint until the interface lookup settles, and keep Step 2 reading as busy while it waits — the sidebar has no separate Generate step the user is expected to reach, so it must still mint on its own, unlike Settings which clears and waits for an explicit press. * fix(mobile): gate the Step 2 mint on this flow visit's address lookup The first attempt gated on a single boolean ref meaning "an address lookup is running". That cannot describe a re-entrant operation: entering the flow, leaving, and re-entering runs two overlapping lookups, and the first to land clears the flag while the second is still out — so the mint went out against the superseded lookup's address and the second lookup then reminted with rotate: true. The same double mint the change exists to remove, one path over. Gate on positive evidence instead. Each flow entry bumps a visit counter; the lookup records the visit it answered (max, so an abandoned visit landing last cannot walk the marker backwards); the mint waits for addressedFlowVisit === pairingFlowVisit, which is false at t=0 by construction and makes exactly one false-to-true transition per visit. The ref is gone and the effect's dependencies now name what it depends on. A superseded lookup's response is also discarded outright, so it cannot move the picker onto an address a newer lookup already replaced — that reselection is itself a remint trigger. The derived busy flag collapses to one clause and is renamed awaitingPairingAddress: it was being passed down as pairLoading while local readers used the real one. It stays separate from pairLoading because that feeds shouldRegenerate in the invalidation hook, where merging them would let a mode switch mint before the address settles. * fix(mobile): put the visit-settled write behind the lookup epoch guard setAddressedFlowVisit was the one completion side-effect outside networkInterfacesRequestIdRef, so a superseded lookup *for the same visit* still marked that visit addressed and released the mint while its own replacement was still pending — the newer address then rotated the offer away. The visit counter cannot see this case: both lookups belong to one visit, and only the request epoch distinguishes them. Reaching it needs a manual Refresh click to beat the commit that disables that button, so field impact is low. The point is that the invariant is now structural instead of resting on a button being disabled in time. Math.max is dropped with the move. Every visit bump starts its own lookup, so the newest request always carries the highest visit and the marker cannot move backwards — the max could no longer be killed by any single mutation, which made it dead code asserting a hazard the guard removes. Also swap the test reset to _resetPairedMobileDevicesCacheForTests, matching the sibling suites: replacePairedMobileDevices is production API that publishes loaded:true and leaves the recovery-listener refcount untouched. * refactor(mobile): make the unaddressed flow visit an explicit null -1 only worked because visits start at 0 and count up; null says "no visit has been addressed yet" without depending on that. Also record at the visit bump why it cannot move into the stage effect: an effect runs a render after Step 2 is visible, so the auto-mint would see the previous visit settled. * fix(mobile): invalidate abandoned pairing mints --- .../src/components/mobile/MobilePage.test.tsx | 352 ++++++++++++++++++ .../src/components/mobile/MobilePage.tsx | 58 ++- 2 files changed, 398 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/components/mobile/MobilePage.test.tsx b/src/renderer/src/components/mobile/MobilePage.test.tsx index 2149cadc284..5805bca46e6 100644 --- a/src/renderer/src/components/mobile/MobilePage.test.tsx +++ b/src/renderer/src/components/mobile/MobilePage.test.tsx @@ -55,7 +55,10 @@ vi.mock('./MobilePageContent', () => ({ onCustomAddressSelect: (address: string) => void onCustomAddressRemove: (address: string) => void beforeCustomAddressChange: (address: string) => Promise + handleBack: () => void handleContinue: () => void + pairAnotherDevice: () => void + pairLoading: boolean pairQrDataUrl: string | null pairQrSize: number | null pairingUrl: string | null @@ -74,6 +77,7 @@ vi.mock('./MobilePageContent', () => ({ {props.stepIdx} {props.connectionMode} {String(props.canGeneratePairing)} + {String(props.pairLoading)} {props.pairQrDataUrl ?? 'none'} {props.pairQrSize ?? 'none'} {props.pairingUrl ?? 'none'} @@ -89,6 +93,12 @@ vi.mock('./MobilePageContent', () => ({ + + @@ -130,6 +140,7 @@ vi.mock('./MobilePageContent', () => ({ })) import MobilePage from './MobilePage' +import { _resetPairedMobileDevicesCacheForTests } from './paired-mobile-devices' describe('MobilePage pairing connection mode', () => { const getPairingQR = vi.fn() @@ -143,6 +154,9 @@ describe('MobilePage pairing connection mode', () => { pairingUrl: 'orca://pair#automatic' }) listNetworkInterfaces.mockReset().mockResolvedValue({ interfaces: [] }) + // The paired-device cache is module state shared by every surface; reset it so + // one test's phones cannot decide the next test's opening stage. + _resetPairedMobileDevicesCacheForTests() mocks.storeState = { closeMobilePage: vi.fn(), orcaProfileAuthStatus: { state: 'connected' }, @@ -540,6 +554,344 @@ describe('MobilePage pairing connection mode', () => { ) }) + it('mints one offer when Continue lands while the address refresh is in flight', async () => { + listNetworkInterfaces.mockResolvedValue({ + interfaces: [{ name: 'Wi-Fi', address: '10.0.0.5' }] + }) + const user = userEvent.setup() + render() + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('intro')) + await user.click(screen.getByRole('button', { name: 'Enter flow' })) + await user.click(screen.getByRole('button', { name: 'Continue' })) + await waitFor(() => + expect(getPairingQR).toHaveBeenCalledWith({ + address: '10.0.0.5', + connectionMode: 'automatic' + }) + ) + + // Leave the flow so re-entering refetches the interface list, and hold that + // refetch open so Continue is clicked while the address is still unsettled. + await user.click(screen.getByRole('button', { name: 'Back' })) + await user.click(screen.getByRole('button', { name: 'Back' })) + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('intro')) + getPairingQR.mockClear() + let resolveRefresh: ((value: Record) => void) | undefined + listNetworkInterfaces.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveRefresh = resolve + }) + ) + await user.click(screen.getByRole('button', { name: 'Enter flow' })) + await user.click(screen.getByRole('button', { name: 'Continue' })) + + // Nothing may be minted yet, and Step 2 must read as busy rather than + // offering "Generate a pairing code" it is about to run itself. + expect(getPairingQR).not.toHaveBeenCalled() + expect(screen.getByTestId('pair-loading')).toHaveTextContent('true') + + // The lease moved while the page was away. + resolveRefresh?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.9' }] }) + await waitFor(() => + expect(screen.getByTestId('selected-address')).toHaveTextContent('10.0.0.9') + ) + // One Continue is one offer. Minting against the stale address and then + // rotating to the settled one runs two overlapping mints through main, whose + // rotate deletes the pending credential the first mint already returned. + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(1)) + expect(getPairingQR).toHaveBeenCalledWith({ + address: '10.0.0.9', + connectionMode: 'automatic' + }) + }) + + it('does not let an abandoned mint populate a new pairing visit', async () => { + const user = userEvent.setup() + let resolveAbandonedMint: ((value: Record) => void) | undefined, + resolveCurrentMint: ((value: Record) => void) | undefined + getPairingQR + .mockImplementationOnce(() => new Promise((resolve) => (resolveAbandonedMint = resolve))) + .mockImplementationOnce(() => new Promise((resolve) => (resolveCurrentMint = resolve))) + await openPairingStep() + + await user.click(screen.getByRole('button', { name: 'Back' })) + await user.click(screen.getByRole('button', { name: 'Back' })) + await user.click(screen.getByRole('button', { name: 'Enter flow' })) + await user.click(screen.getByRole('button', { name: 'Continue' })) + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(2)) + + resolveAbandonedMint?.({ available: true, qrDataUrl: 'abandoned' }) + + resolveCurrentMint?.({ available: true, qrDataUrl: 'current' }) + await waitFor(() => expect(screen.getByTestId('pairing-qr')).toHaveTextContent('current')) + }) + + it('mints "Pair another device" against the resolved address, not the default', async () => { + window.api.mobile.listDevices = vi.fn().mockResolvedValue({ + devices: [{ deviceId: 'phone-1', name: 'Pixel', pairedAt: 1, lastSeenAt: 2 }] + }) + listNetworkInterfaces.mockResolvedValue({ + interfaces: [{ name: 'Wi-Fi', address: '10.0.0.5' }] + }) + const user = userEvent.setup() + render() + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('paired')) + + // This jumps straight to Step 2 in the same commit that starts the interface + // lookup, so the auto-mint always runs before any address is known. + await user.click(screen.getByRole('button', { name: 'Pair another device' })) + + await waitFor(() => + expect(screen.getByTestId('selected-address')).toHaveTextContent('10.0.0.5') + ) + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(1)) + expect(getPairingQR).toHaveBeenCalledWith({ + address: '10.0.0.5', + connectionMode: 'automatic' + }) + + // Returning to the paired list and pairing again is a fresh visit: it must + // wait for its own lookup, not inherit the previous visit's answer. + await user.click(screen.getByRole('button', { name: 'Back' })) + await user.click(screen.getByRole('button', { name: 'Back' })) + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('paired')) + getPairingQR.mockClear() + let resolveSecondLookup: ((value: Record) => void) | undefined + listNetworkInterfaces.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveSecondLookup = resolve + }) + ) + await user.click(screen.getByRole('button', { name: 'Pair another device' })) + expect(getPairingQR).not.toHaveBeenCalled() + + resolveSecondLookup?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.9' }] }) + await waitFor(() => + expect(screen.getByTestId('selected-address')).toHaveTextContent('10.0.0.9') + ) + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(1)) + expect(getPairingQR).toHaveBeenCalledWith({ + address: '10.0.0.9', + connectionMode: 'automatic' + }) + }) + + it('waits for the newest lookup when the flow is re-entered mid-refresh', async () => { + const user = userEvent.setup() + let resolveFirstLookup: ((value: Record) => void) | undefined + let resolveSecondLookup: ((value: Record) => void) | undefined + listNetworkInterfaces + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveFirstLookup = resolve + }) + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveSecondLookup = resolve + }) + ) + render() + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('intro')) + + // Enter, leave, and re-enter while the first lookup is still unanswered, so + // two lookups overlap and the older one is the first to settle. + await user.click(screen.getByRole('button', { name: 'Enter flow' })) + await user.click(screen.getByRole('button', { name: 'Back' })) + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('intro')) + await user.click(screen.getByRole('button', { name: 'Enter flow' })) + await user.click(screen.getByRole('button', { name: 'Continue' })) + await waitFor(() => expect(listNetworkInterfaces).toHaveBeenCalledTimes(2)) + + // The superseded lookup answers first. It must neither move the picker nor + // release the mint — this visit's lookup has not answered yet. + resolveFirstLookup?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.5' }] }) + await waitFor(() => expect(screen.getByTestId('pair-loading')).toHaveTextContent('true')) + expect(getPairingQR).not.toHaveBeenCalled() + expect(screen.getByTestId('selected-address')).toHaveTextContent('none') + + resolveSecondLookup?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.9' }] }) + await waitFor(() => + expect(screen.getByTestId('selected-address')).toHaveTextContent('10.0.0.9') + ) + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(1)) + expect(getPairingQR).toHaveBeenCalledWith({ + address: '10.0.0.9', + connectionMode: 'automatic' + }) + }) + + it('does not release the mint when a superseded lookup for the same visit settles', async () => { + const user = userEvent.setup() + let resolveEntryLookup: ((value: Record) => void) | undefined + let resolveManualLookup: ((value: Record) => void) | undefined + listNetworkInterfaces + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveEntryLookup = resolve + }) + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveManualLookup = resolve + }) + ) + render() + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('intro')) + await user.click(screen.getByRole('button', { name: 'Enter flow' })) + await user.click(screen.getByRole('button', { name: 'Continue' })) + + // A manual refresh overlaps the entry lookup, so both belong to this visit — + // the visit counter cannot tell them apart, only the request epoch can. + await user.click(screen.getByRole('button', { name: 'Refresh addresses' })) + await waitFor(() => expect(listNetworkInterfaces).toHaveBeenCalledTimes(2)) + + // The superseded lookup answers first. Marking the visit addressed here would + // release the mint against an address its own replacement is about to change. + resolveEntryLookup?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.5' }] }) + await waitFor(() => expect(screen.getByTestId('pair-loading')).toHaveTextContent('true')) + expect(getPairingQR).not.toHaveBeenCalled() + expect(screen.getByTestId('selected-address')).toHaveTextContent('none') + + resolveManualLookup?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.9' }] }) + await waitFor(() => + expect(screen.getByTestId('selected-address')).toHaveTextContent('10.0.0.9') + ) + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(1)) + expect(getPairingQR).toHaveBeenCalledWith({ + address: '10.0.0.9', + connectionMode: 'automatic' + }) + }) + + it('does not re-block Step 2 when an abandoned visit’s lookup settles last', async () => { + const user = userEvent.setup() + let resolveAbandonedLookup: ((value: Record) => void) | undefined + let resolveCurrentLookup: ((value: Record) => void) | undefined + listNetworkInterfaces + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveAbandonedLookup = resolve + }) + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveCurrentLookup = resolve + }) + ) + render() + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('intro')) + await user.click(screen.getByRole('button', { name: 'Enter flow' })) + await user.click(screen.getByRole('button', { name: 'Back' })) + await waitFor(() => expect(screen.getByTestId('stage')).toHaveTextContent('intro')) + await user.click(screen.getByRole('button', { name: 'Enter flow' })) + await user.click(screen.getByRole('button', { name: 'Continue' })) + await waitFor(() => expect(listNetworkInterfaces).toHaveBeenCalledTimes(2)) + + resolveCurrentLookup?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.9' }] }) + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(screen.getByTestId('pair-loading')).toHaveTextContent('false')) + + // The abandoned visit answers last. Recording it as the settled visit would + // walk the marker backwards and leave Step 2 waiting on a lookup that is + // never coming, with its Generate action disabled. + resolveAbandonedLookup?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.5' }] }) + await waitFor(() => + expect(screen.getByTestId('refreshing-addresses')).toHaveTextContent('false') + ) + expect(screen.getByTestId('pair-loading')).toHaveTextContent('false') + expect(screen.getByTestId('selected-address')).toHaveTextContent('10.0.0.9') + expect(getPairingQR).toHaveBeenCalledTimes(1) + }) + + it('ignores an interface lookup that settles after a newer one', async () => { + listNetworkInterfaces.mockResolvedValue({ + interfaces: [{ name: 'Wi-Fi', address: '10.0.0.5' }] + }) + const user = userEvent.setup() + await openPairingStep() + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(1)) + + // Two manual refreshes overlap; the older one answers last with a stale list. + let resolveStaleRefresh: ((value: Record) => void) | undefined + listNetworkInterfaces.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveStaleRefresh = resolve + }) + ) + await user.click(screen.getByRole('button', { name: 'Refresh addresses' })) + listNetworkInterfaces.mockResolvedValueOnce({ + interfaces: [{ name: 'Wi-Fi', address: '10.0.0.7' }] + }) + await user.click(screen.getByRole('button', { name: 'Refresh addresses' })) + await waitFor(() => + expect(screen.getByTestId('selected-address')).toHaveTextContent('10.0.0.7') + ) + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(2)) + expect(getPairingQR).toHaveBeenLastCalledWith({ + address: '10.0.0.7', + connectionMode: 'automatic', + rotate: true + }) + + resolveStaleRefresh?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.1' }] }) + await waitFor(() => + expect(screen.getByTestId('refreshing-addresses')).toHaveTextContent('false') + ) + + // The stale list must not reselect an address and rotate the live offer away. + expect(screen.getByTestId('selected-address')).toHaveTextContent('10.0.0.7') + expect(getPairingQR).toHaveBeenCalledTimes(2) + }) + + it('does not report the pairing step as busy during a manual address refresh', async () => { + listNetworkInterfaces.mockResolvedValue({ + interfaces: [{ name: 'Wi-Fi', address: '10.0.0.5' }] + }) + // Leave Step 2 with no QR on screen: that is the state where a refresh could + // be mistaken for a mint in progress. + getPairingQR.mockResolvedValueOnce({ + available: false, + reason: 'websocket_unavailable', + guidance: 'WebSocket transport is not running' + }) + const user = userEvent.setup() + await openPairingStep() + await waitFor(() => expect(getPairingQR).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(screen.getByTestId('pair-loading')).toHaveTextContent('false')) + expect(screen.getByTestId('pairing-qr')).toHaveTextContent('none') + expect(screen.getByTestId('relay-failure')).toHaveTextContent('none') + + let resolveRefresh: ((value: Record) => void) | undefined + listNetworkInterfaces.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveRefresh = resolve + }) + ) + await user.click(screen.getByRole('button', { name: 'Refresh addresses' })) + await waitFor(() => + expect(screen.getByTestId('refreshing-addresses')).toHaveTextContent('true') + ) + + // Nothing is minting, so Step 2 must not claim it is — that would disable the + // Generate action while the user is only re-reading the interface list. + expect(screen.getByTestId('pair-loading')).toHaveTextContent('false') + resolveRefresh?.({ interfaces: [{ name: 'Wi-Fi', address: '10.0.0.5' }] }) + await waitFor(() => + expect(screen.getByTestId('refreshing-addresses')).toHaveTextContent('false') + ) + }) + it('keeps custom intent when the saved address is also discovered', async () => { mocks.storeState.settings = { showMobileButton: true, diff --git a/src/renderer/src/components/mobile/MobilePage.tsx b/src/renderer/src/components/mobile/MobilePage.tsx index 434f0298626..416efd2ac30 100644 --- a/src/renderer/src/components/mobile/MobilePage.tsx +++ b/src/renderer/src/components/mobile/MobilePage.tsx @@ -62,6 +62,13 @@ export default function MobilePage(): React.JSX.Element { const [refreshingNetworkInterfaces, setRefreshingNetworkInterfaces] = useState(false) const hasGeneratedRef = useRef(false) const pairingRequestIdRef = useRef(0) + // Why: each flow entry starts its own address lookup. Gating the Step 2 mint on + // "has this visit's lookup settled" is false until it answers, where "is a lookup + // running" cannot tell overlapping lookups apart and clears on the first to land. + const [pairingFlowVisit, setPairingFlowVisit] = useState(0) + const [addressedFlowVisit, setAddressedFlowVisit] = useState(null) + const pairingAddressSettled = addressedFlowVisit === pairingFlowVisit + const networkInterfacesRequestIdRef = useRef(0) const mountedRef = useMountedRef() const closeMobilePage = useAppStore((s) => s.closeMobilePage) const showMobileButton = useAppStore((s) => s.settings?.showMobileButton !== false) @@ -188,23 +195,33 @@ export default function MobilePage(): React.JSX.Element { }) const loadNetworkInterfaces = useCallback(async () => { + const requestId = ++networkInterfacesRequestIdRef.current + const visit = pairingFlowVisit if (mountedRef.current) { setRefreshingNetworkInterfaces(true) } try { const result = await window.api.mobile.listNetworkInterfaces() - if (mountedRef.current) { + // Why: a superseded lookup must not move the selection a newer one already + // resolved — that address change remints over the offer just advertised. + if (mountedRef.current && requestId === networkInterfacesRequestIdRef.current) { setNetworkInterfaces(result.interfaces) selectAddressAfterRefresh(result.interfaces) } } catch { // Network list is non-critical; the QR will still mint with default routing. } finally { - if (mountedRef.current) { + // Why: only the newest lookup may report a completion — a superseded one + // marking its visit addressed releases the mint against an address its own + // replacement is about to change. Plain assignment is safe because entering + // a flow bumps the visit and starts its own lookup, so the newest request + // always carries the highest visit. + if (mountedRef.current && requestId === networkInterfacesRequestIdRef.current) { + setAddressedFlowVisit(visit) setRefreshingNetworkInterfaces(false) } } - }, [mountedRef, selectAddressAfterRefresh]) + }, [mountedRef, pairingFlowVisit, selectAddressAfterRefresh]) useEffect(() => { if (stage !== 'flow') { @@ -263,30 +280,39 @@ export default function MobilePage(): React.JSX.Element { if (!canGenerate) { return } + // Why: entering Step 2 also starts this visit's address lookup, and minting + // before it settles advertises an address the lookup is about to replace — the + // replacement then rotates away the credential this mint just created, so one + // Continue runs two overlapping offers through main for one pending token. + if (!pairingAddressSettled) { + return + } void generatePairing(false) - }, [stage, stepIdx, canGenerate, generatePairing]) + }, [stage, stepIdx, canGenerate, generatePairing, pairingAddressSettled]) // Why: entering the flow must mint a fresh pairing token — clear stale QR // state so we never flash an expired code from a previous session. - const enterFlow = (): void => { + const beginPairingVisit = (): void => { + pairingRequestIdRef.current += 1 + setPairLoading(false) + setPairingFlowVisit((visit) => visit + 1) hasGeneratedRef.current = false setPairQrDataUrl(null) setPairQrSize(null) setPairingUrl(null) setPairingQrError(false) setRelayMintFailure(null) + } + + const enterFlow = (): void => { + beginPairingVisit() showFirstPairingFlow() } // Why: from the paired summary, "Pair another device" jumps straight to // Step 2 since the app is presumably already installed on the user's phone. const pairAnotherDevice = (): void => { - hasGeneratedRef.current = false - setPairQrDataUrl(null) - setPairQrSize(null) - setPairingUrl(null) - setPairingQrError(false) - setRelayMintFailure(null) + beginPairingVisit() showPairAnotherDeviceFlow() } @@ -311,6 +337,14 @@ export default function MobilePage(): React.JSX.Element { useMobilePageEscape(closeMobilePage) + // Why: while the deferred first mint waits on the address, Step 2 would + // otherwise read "Generate a pairing code to continue" — a prompt for work it + // is already about to do on the user's behalf. Kept separate from pairLoading: + // that one feeds the invalidation hook's shouldRegenerate, so folding this into + // it would let a mode switch mint before the address settles. + const awaitingPairingAddress = + stage === 'flow' && stepIdx === 1 && canGenerate && !pairingAddressSettled + return ( Date: Thu, 17 Sep 2026 21:21:01 -0700 Subject: [PATCH 41/59] fix(terminal): keep a split's real direction when the leaf set moves (#21294) resolveTerminalLayoutRoot discarded any known tree that did not cover the published leaf set exactly and rebuilt the tab as a flat chain with a guessed 'horizontal' direction, restacking side-by-side panes. The guess is then published, mirrored to every paired client, and written back over the real tree, so the direction is gone from disk. Prune a known tree to the leaves that survive and graft only the leaves no tree places, which is now the sole place a direction is invented and is still reported through onSynthesize. --- config/scripts/pr-e2e-source-routing.mjs | 11 + .../remote-terminal-layout-resolution.test.ts | 88 ++++++++ .../remote-terminal-layout-resolution.ts | 102 ++++++--- .../sync-runtime-graph/graph-publication.ts | 2 +- .../mobile-session-terminal-tabs.ts | 2 +- .../terminal-surfaces.ts | 4 +- ...shed-split-orientation-legacy-leaf.spec.ts | 202 ++++++++++++++++++ 7 files changed, 378 insertions(+), 33 deletions(-) create mode 100644 tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts diff --git a/config/scripts/pr-e2e-source-routing.mjs b/config/scripts/pr-e2e-source-routing.mjs index 3b8f2e90afb..b9c7c387716 100644 --- a/config/scripts/pr-e2e-source-routing.mjs +++ b/config/scripts/pr-e2e-source-routing.mjs @@ -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'], diff --git a/src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts b/src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts index a47d09350ad..2c1f100cd52 100644 --- a/src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts +++ b/src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts @@ -69,6 +69,94 @@ describe('resolveTerminalLayoutRoot', () => { expect(resolveTerminalLayoutRoot({ leafIds: [] })).toBeNull() }) + it('prunes a superset tree to the live leaves instead of re-guessing its directions', () => { + // A stale/extra leaf in the known tree used to fail the exact-cover check and + // collapse the whole tab to a guessed chain. + const onSynthesize = vi.fn() + const root = resolveTerminalLayoutRoot({ + authoritativeRoot: { + type: 'split', + direction: 'horizontal', + first: verticalSplit, + second: { type: 'leaf', leafId: 'stale' } + }, + leafIds: ['a', 'b'], + onSynthesize + }) + expect(root).toEqual(verticalSplit) + expect(onSynthesize).not.toHaveBeenCalled() + }) + + it('collapses a split that loses one child and keeps the outer direction', () => { + const root = resolveTerminalLayoutRoot({ + authoritativeRoot: { + type: 'split', + direction: 'vertical', + first: { + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', leafId: 'a' }, + second: { type: 'leaf', leafId: 'b' } + }, + second: { type: 'leaf', leafId: 'c' } + }, + leafIds: ['a', 'c'] + }) + expect(root).toEqual({ + type: 'split', + direction: 'vertical', + first: { type: 'leaf', leafId: 'a' }, + second: { type: 'leaf', leafId: 'c' } + }) + }) + + it('grafts a genuinely new leaf without disturbing the directions already known', () => { + // Only the new leaf's placement is a guess; the vertical split must survive it. + const onSynthesize = vi.fn() + const root = resolveTerminalLayoutRoot({ + authoritativeRoot: verticalSplit, + leafIds: ['a', 'b', 'c'], + onSynthesize + }) + expect(root).toEqual({ + type: 'split', + direction: 'horizontal', + first: verticalSplit, + second: { type: 'leaf', leafId: 'c' } + }) + expect(onSynthesize).toHaveBeenCalledWith(1) + }) + + it('keeps the prior client tree when the host tree places fewer of the leaves', () => { + const root = resolveTerminalLayoutRoot({ + authoritativeRoot: { type: 'leaf', leafId: 'a' }, + existingRoot: verticalSplit, + leafIds: ['a', 'b', 'c'] + }) + expect(root).toEqual({ + type: 'split', + direction: 'horizontal', + first: verticalSplit, + second: { type: 'leaf', leafId: 'c' } + }) + }) + + it('still degenerates when no known tree places any of the leaves', () => { + const onSynthesize = vi.fn() + const root = resolveTerminalLayoutRoot({ + authoritativeRoot: verticalSplit, + leafIds: ['x', 'y'], + onSynthesize + }) + expect(root).toEqual({ + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', leafId: 'x' }, + second: { type: 'leaf', leafId: 'y' } + }) + expect(onSynthesize).toHaveBeenCalledWith(2) + }) + it('prefers authoritative over an also-covering existing tree', () => { const horizontalSplit: TerminalPaneLayoutNode = { type: 'split', diff --git a/src/renderer/src/runtime/remote-terminal-layout-resolution.ts b/src/renderer/src/runtime/remote-terminal-layout-resolution.ts index 1691132227b..918697feeeb 100644 --- a/src/renderer/src/runtime/remote-terminal-layout-resolution.ts +++ b/src/renderer/src/runtime/remote-terminal-layout-resolution.ts @@ -7,11 +7,13 @@ import type { TerminalPaneLayoutNode } from '../../../shared/terminal-tab-types' * of independently re-deriving it (which is how "Split Right" used to render as * a down split — divergent fallbacks each guessed a direction). * - * Invariant: NEVER invent a split direction. A split's direction is meaningful - * user/host state, so a guessed direction is wrong by construction. When no - * authoritative tree covers the leaves, we keep whatever covering tree we do - * have, and only as a true last resort synthesize a degenerate chain — logged - * so the gap is visible rather than silently masquerading as a real layout. + * Invariant: NEVER invent a split direction for a leaf some known tree already + * places. A split's direction is meaningful user/host state, and the resolved + * tree is persisted and pushed back to the host, so a guess that wins here + * destroys the real direction on disk — a one-way door. A known tree that does + * not match the leaf set exactly is still knowledge: it is pruned to the leaves + * that survive and grafted with only the genuinely new ones, which is the sole + * place a direction is invented (and always reported). */ function collectLayoutLeafIds( @@ -47,40 +49,63 @@ export function layoutCoversLeaves( } /** - * Last-resort tree when no authoritative or prior layout covers the leaves. - * A single leaf needs no direction; >1 leaf cannot be rendered as a split - * without inventing one, so this path is degenerate and should not fire for a - * real split — callers pass `onSynthesize` to surface when it does. + * Drop every leaf outside `keep`; a split that loses one child collapses to the + * other. Surviving splits keep the direction the user/host actually chose. */ -function synthesizeDegenerateLayout( - leafIds: readonly string[], - onSynthesize?: (leafCount: number) => void +export function pruneLayoutToLeaves( + node: TerminalPaneLayoutNode | null | undefined, + keep: ReadonlySet ): TerminalPaneLayoutNode | null { - if (leafIds.length === 0) { + if (!node) { return null } - if (leafIds.length === 1) { - return { type: 'leaf', leafId: leafIds[0]! } + if (node.type === 'leaf') { + return keep.has(node.leafId) ? node : null } - onSynthesize?.(leafIds.length) - // No known direction: stack left-to-right as a flat chain. This is a visible - // fallback, not a guess we want to win — see invariant above. - return leafIds.slice(1).reduce( - (root, leafId) => ({ - type: 'split', - direction: 'horizontal', - first: root, - second: { type: 'leaf', leafId } - }), - { type: 'leaf', leafId: leafIds[0]! } + const first = pruneLayoutToLeaves(node.first, keep) + const second = pruneLayoutToLeaves(node.second, keep) + if (first && second) { + return first === node.first && second === node.second ? node : { ...node, first, second } + } + return first ?? second +} + +/** + * Attach leaves no known tree describes. This is the only direction we invent, + * so it is always reported; the retained subtree keeps its real directions. + */ +function graftUnplacedLeaves( + root: TerminalPaneLayoutNode | null, + unplacedLeafIds: readonly string[] +): TerminalPaneLayoutNode | null { + return unplacedLeafIds.reduce( + (tree, leafId) => + tree === null + ? { type: 'leaf', leafId } + : { type: 'split', direction: 'horizontal', first: tree, second: { type: 'leaf', leafId } }, + root ) } +/** How many of `leafIds` this tree already places — its value as a donor. */ +function countPlacedLeaves( + root: TerminalPaneLayoutNode | null | undefined, + leafIds: readonly string[] +): number { + if (!root) { + return 0 + } + const treeLeafIds = collectLayoutLeafIds(root) + return leafIds.filter((leafId) => treeLeafIds.has(leafId)).length +} + /** * Resolve the layout tree for `leafIds`, preferring authoritative/known trees - * (which carry the real direction) over any synthesized fallback. + * (which carry the real direction) over any invented structure. * - * Precedence: host-authoritative layout → prior client layout → degenerate. + * Precedence: a tree covering the leaves exactly (host-authoritative, then + * prior client) → the tree placing the most leaves, pruned to them and grafted + * with the rest → a degenerate chain when nothing is known. */ export function resolveTerminalLayoutRoot(args: { authoritativeRoot?: TerminalPaneLayoutNode | null @@ -94,5 +119,24 @@ export function resolveTerminalLayoutRoot(args: { if (layoutCoversLeaves(args.existingRoot, args.leafIds)) { return args.existingRoot ?? null } - return synthesizeDegenerateLayout(args.leafIds, args.onSynthesize) + if (args.leafIds.length === 0) { + return null + } + const authoritativePlaced = countPlacedLeaves(args.authoritativeRoot, args.leafIds) + const existingPlaced = countPlacedLeaves(args.existingRoot, args.leafIds) + // Ties go to the host tree; it is the authority for direction. + const donor = + authoritativePlaced === 0 && existingPlaced === 0 + ? null + : authoritativePlaced >= existingPlaced + ? args.authoritativeRoot + : args.existingRoot + const retained = pruneLayoutToLeaves(donor, new Set(args.leafIds)) + const placed = collectLayoutLeafIds(retained) + const unplaced = args.leafIds.filter((leafId) => !placed.has(leafId)) + // One leaf and nothing retained is a bare leaf, which carries no direction. + if (unplaced.length > (retained === null ? 1 : 0)) { + args.onSynthesize?.(unplaced.length) + } + return graftUnplacedLeaves(retained, unplaced) } diff --git a/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts b/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts index f6457fcc454..f74c97f6e41 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts @@ -165,7 +165,7 @@ export async function syncRuntimeGraph(): Promise { leafIds: liveLeaves.map(([leafId]) => leafId), onSynthesize: (leafCount) => console.warn( - `[sync-runtime-graph] synthesized layout for ${leafCount} unmounted leaves with no saved tree` + `[sync-runtime-graph] synthesized a split direction for ${leafCount} unmounted leaves no saved tree placed` ) }) }) diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-terminal-tabs.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-terminal-tabs.ts index b1c7fe4b54f..ac88ad17eb6 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-terminal-tabs.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-terminal-tabs.ts @@ -49,7 +49,7 @@ export function buildMobileTerminalSurfaceTabs( leafIds, onSynthesize: (leafCount) => console.warn( - `[sync-runtime-graph] synthesized parentLayout for ${leafCount} leaves with no live or saved tree` + `[sync-runtime-graph] synthesized a parentLayout split direction for ${leafCount} leaves no live or saved tree placed` ) }), activeLeafId, diff --git a/src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts b/src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts index 78169094429..233993ec171 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts @@ -201,14 +201,14 @@ export function chooseRemoteTerminalLayout( ? parentLayout.expandedLeafId : null return { - // Why: host parentLayout is authoritative for split direction; else keep the prior client tree, then degenerate — never re-guess a direction. + // Why: host parentLayout is authoritative for split direction; else keep the prior client tree — a leaf-set mismatch prunes/grafts it, never re-guesses the directions it already carries. root: resolveTerminalLayoutRoot({ authoritativeRoot: parentLayout?.root, existingRoot: existingLayout?.root, leafIds, onSynthesize: (leafCount) => console.warn( - `[web-session-tabs-sync] synthesized layout for ${leafCount} leaves; no authoritative or prior tree covered them` + `[web-session-tabs-sync] synthesized a split direction for ${leafCount} leaves no authoritative or prior tree placed` ) }), activeLeafId, diff --git a/tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts b/tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts new file mode 100644 index 00000000000..0a0ff09f07a --- /dev/null +++ b/tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts @@ -0,0 +1,202 @@ +import type { Page } from '@stablyai/playwright-test' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../src/shared/terminal-tab-types' +import { expect, test } from './helpers/orca-app' +import { + callPairedRuntime, + waitForPairedClientWorktree +} from './helpers/paired-client-host-session' +import { + createRuntimeDesktopPairingOffer, + launchPairedElectronClient, + type PairedElectronClient +} from './helpers/paired-electron-client' +import { parkHiddenTabBehindDecoy } from './helpers/terminal-hidden-parking' +import { + readPaneIdentitySnapshot, + resolveActiveTabId, + splitActiveTerminalPane, + waitForActiveTerminalManager, + waitForPaneCount +} from './helpers/terminal' + +/** + * A desktop app republishes an unmounted terminal tab's layout from its own saved tree, and + * derives the leaf set from that tree's stable-id leaves. A leaf id that predates the stable-id + * scheme drops out of the leaf set but stays in the tree, so the tree stopped covering the leaf + * set exactly — and the publisher used to answer that by discarding the tree and chaining every + * leaf with a guessed "horizontal", restacking a side-by-side split for every paired client and + * for the record they all write back. The real direction has to survive the mismatch. + */ + +/** Legacy pane id shape: not a stable pane UUID, so it never reaches the published leaf set. */ +const LEGACY_LEAF_ID = 'pane:9' + +/** + * Shrinks both the cold-park delay and the hot-retain window. Set at module scope because the + * `orcaPage` fixture launches the app before any test body runs. + */ +const PARK_DELAY_MS = 2_000 +process.env.ORCA_E2E_TERMINAL_PARKING_DELAY_MS ??= String(PARK_DELAY_MS) + +function collectLeafIds(node: TerminalPaneLayoutNode | null | undefined): string[] { + if (!node) { + return [] + } + return node.type === 'leaf' + ? [node.leafId] + : [...collectLeafIds(node.first), ...collectLeafIds(node.second)] +} + +/** Direction of the split that separates the two leaves, or null if one side holds both. */ +function splitDirectionSeparating( + node: TerminalPaneLayoutNode | null | undefined, + leafA: string, + leafB: string +): 'horizontal' | 'vertical' | null { + if (!node || node.type === 'leaf') { + return null + } + const firstLeaves = new Set(collectLeafIds(node.first)) + const secondLeaves = new Set(collectLeafIds(node.second)) + if ( + (firstLeaves.has(leafA) && secondLeaves.has(leafB)) || + (firstLeaves.has(leafB) && secondLeaves.has(leafA)) + ) { + return node.direction + } + return ( + splitDirectionSeparating(node.first, leafA, leafB) ?? + splitDirectionSeparating(node.second, leafA, leafB) + ) +} + +function readSavedLayout(page: Page, tabId: string): Promise { + return page.evaluate((id) => window.__store?.getState().terminalLayoutsByTabId[id] ?? null, tabId) +} + +type PublishedTerminalSurface = { + type: string + parentTabId?: string + leafId?: string + parentLayout?: TerminalLayoutSnapshot +} + +async function readPublishedTerminalSurfaces( + client: PairedElectronClient, + worktreeId: string, + hostTabId: string +): Promise { + const snapshot = await callPairedRuntime<{ tabs: PublishedTerminalSurface[] }>( + client.page, + client.environmentId, + 'session.tabs.list', + { worktree: `id:${worktreeId}` } + ) + return snapshot.tabs.filter((tab) => tab.type === 'terminal' && tab.parentTabId === hostTabId) +} + +test('publishes an unmounted split with its real orientation when a legacy leaf lingers in the saved tree', async ({ + orcaPage +}, testInfo) => { + test.setTimeout(360_000) + const worktreeId = await orcaPage.evaluate(() => window.__store?.getState().activeWorktreeId) + if (!worktreeId) { + throw new Error('Headed host has no active seeded workspace') + } + let client: PairedElectronClient | null = null + + try { + await waitForActiveTerminalManager(orcaPage, 60_000) + const hostTabId = await resolveActiveTabId(orcaPage) + if (!hostTabId) { + throw new Error('Headed host has no active terminal tab') + } + + // Split right: two panes side by side, the orientation the report is about. + await splitActiveTerminalPane(orcaPage, 'vertical') + await waitForPaneCount(orcaPage, 2, 60_000) + const panes = await readPaneIdentitySnapshot(orcaPage) + const leafIds = (panes?.panes ?? []).map((pane) => pane.leafId) + const [firstLeafId, secondLeafId] = leafIds + if (leafIds.length !== 2 || !firstLeafId || !secondLeafId) { + throw new Error(`Expected two split leaves, saw ${JSON.stringify(leafIds)}`) + } + + await expect + .poll( + async () => + splitDirectionSeparating( + (await readSavedLayout(orcaPage, hostTabId))?.root, + firstLeafId, + secondLeafId + ), + { timeout: 60_000, message: 'host never saved the side-by-side split' } + ) + .toBe('vertical') + + // Park the tab: a parked tab is republished from the saved tree, not the live DOM. + await parkHiddenTabBehindDecoy(orcaPage, worktreeId, hostTabId, { + parkDelayMs: PARK_DELAY_MS + }) + + // The drift under test: the saved tree keeps a leaf the stable-id leaf set cannot carry. + await orcaPage.evaluate( + ({ tabId, firstLeafId, secondLeafId, legacyLeafId }) => { + const state = window.__store?.getState() + const saved = state?.terminalLayoutsByTabId[tabId] + if (!state || !saved) { + throw new Error('No saved layout to seed the legacy leaf into') + } + state.setTabLayout(tabId, { + ...saved, + root: { + type: 'split', + direction: 'horizontal', + first: { + type: 'split', + direction: 'vertical', + first: { type: 'leaf', leafId: firstLeafId }, + second: { type: 'leaf', leafId: secondLeafId } + }, + second: { type: 'leaf', leafId: legacyLeafId } + } + }) + }, + { tabId: hostTabId, firstLeafId, secondLeafId, legacyLeafId: LEGACY_LEAF_ID } + ) + // Control: with no lingering leaf the saved tree covers the leaf set and the publisher + // never reaches the fallback at all, so the assertions below pass for free. + expect(collectLeafIds((await readSavedLayout(orcaPage, hostTabId))?.root)).toContain( + LEGACY_LEAF_ID + ) + + const offer = await createRuntimeDesktopPairingOffer(orcaPage) + client = await launchPairedElectronClient(offer, testInfo, 'legacy-leaf-orientation-observer') + await waitForPairedClientWorktree(client.page, worktreeId) + + await expect + .poll( + async () => + (await readPublishedTerminalSurfaces(client!, worktreeId, hostTabId)) + .map((surface) => surface.leafId) + .filter((leafId): leafId is string => typeof leafId === 'string') + .sort(), + { + timeout: 90_000, + message: 'host never published both split leaves to the paired client' + } + ) + .toEqual(expect.arrayContaining([firstLeafId, secondLeafId].sort())) + const published = await readPublishedTerminalSurfaces(client, worktreeId, hostTabId) + // Control: the leaf set really does exclude the leaf the saved tree still carries, so the + // publisher reached the mismatch path instead of using the tree verbatim. + expect(published.map((surface) => surface.leafId)).not.toContain(LEGACY_LEAF_ID) + const publishedRoot = published.find((surface) => surface.parentLayout)?.parentLayout?.root + expect(splitDirectionSeparating(publishedRoot, firstLeafId, secondLeafId)).toBe('vertical') + } finally { + await client?.dispose() + } +}) From 5c8540948d41e2c81f0b630811d4610a4a1b8fb8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:21:14 -0700 Subject: [PATCH 42/59] test(e2e): name the paired-client quit that preserves the profile (#21300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quit-without-deleting is closeElectronAppForE2E + cleanupE2EDaemons — dispose's first two steps without removeProfile. The composition is correct today but undiscoverable, and getting it wrong is silent and expensive in both directions. dispose() + reuseUserDataDir yields a FIRST RUN on an empty profile, so every persistence assertion after it reads empty and is indistinguishable from data loss. That produced a phantom data-loss report, live in two write-ups before a diagnostic listing zero session FILES (rather than zero buffers) contradicted it. Reaching for a bare app.close() to skip the deletion hangs instead: it lacks the timeout and force-kill fallback that closeElectronAppForE2E wraps around it, and burned a ten minute test deadline producing no reading at all. Test infrastructure only; no production code. Unblocks restart-persistence coverage for the paired topology. --- tests/e2e/helpers/paired-electron-client.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/e2e/helpers/paired-electron-client.ts b/tests/e2e/helpers/paired-electron-client.ts index a5947bc1228..46597415668 100644 --- a/tests/e2e/helpers/paired-electron-client.ts +++ b/tests/e2e/helpers/paired-electron-client.ts @@ -34,7 +34,15 @@ export type PairedElectronClient = { page: Page environmentId: string captureDirectSshAttempts: () => Promise + /** Closes the app AND deletes the profile. For a restart, use `quitPreservingProfile`. */ dispose: () => Promise + /** Quit for a relaunch on the same profile: everything `dispose` does except `removeProfile`. + * Why named rather than left to callers: composing it wrong is silent and expensive. Calling + * `dispose` and relaunching with `reuseUserDataDir` yields a FIRST RUN on an empty profile, so + * every persistence assertion after it reads empty and looks exactly like data loss — that + * produced a phantom data-loss report once. Reaching for a bare `app.close()` instead hangs: + * it lacks the timeout and force-kill fallback that `closeElectronAppForE2E` wraps around it. */ + quitPreservingProfile: () => Promise getDirectSshAttemptTargetIds: () => Promise installDirectSshAttemptProbe: () => Promise replacePairingInPlace: (offer: RuntimeDesktopPairingOffer) => Promise @@ -240,6 +248,10 @@ export async function launchPairedElectronClient( await cleanupE2EDaemons(userDataDir) await removeProfile(userDataDir) }, + quitPreservingProfile: async () => { + await closeElectronAppForE2E(app) + await cleanupE2EDaemons(userDataDir) + }, getDirectSshAttemptTargetIds: async () => { return readDirectSshAttemptTargetIds(directSshProbePath).filter( (targetId) => targetId !== DIRECT_SSH_PROBE_CANARY_TARGET_ID From 78a17bb24de085992edeb38ce47f6b71227e97da Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:21:27 -0700 Subject: [PATCH 43/59] fix(relay): one malformed pre-auth handshake frame closes its connection, not the daemon (#19879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(relay): one malformed pre-auth handshake frame closes its connection, not the daemon parseHandshakeMessage returned whatever JSON.parse produced, and the daemon interpolates the peer's version into a log line before any credential check. A version that is an object with a non-callable toString throws TypeError there, inside the frame-decoder callback. FrameDecoder.drainTurn wrapped its synchronous dispatch in try/finally with no catch, so the throw escaped feed(), escaped the socket data handler, and reached uncaughtException: the relay daemon exited and every PTY and agent session it held died with it. Two layers, because only the second closes the class: - parseHandshakeMessage now requires the string fields each arm carries (version; expected/got) and rejects a non-object payload. Both readers share the parser, so neither side can interpolate a non-string again. - FrameDecoder contains a frame owner that throws on the synchronous turn the same way it already contained one on a continuation turn: reset the residue and report one FrameDecoderContinuationError to onError. Every owner's onError already closes its own connection, so any future throw of this shape costs one connection instead of the process. The relay CLI channel gains an explicit onError so a malformed reply still ends that one-shot command instead of parking it. * fix(relay): keep the diagnostic the refusal path exists to produce Two error paths that destroy their own evidence. `parseHandshakeMessage`'s unknown-type refusal interpolated `String(t)` on a peer-supplied value: `{"type":{"toString":1}}` makes String() throw "Cannot convert object to primitive value", so the refusal arrives without naming what was refused. `describeRelayProtocolVersion` guards this exact hazard two files away; the sibling was missed. `runRelayOrcaCliChannel`'s new `onDecodeError` wrote to stderr and then exited synchronously. stderr is async on a pipe transport, so the one line recording why the command died could be dropped — the reason relay-handshake.ts already exits inside its write callback. * fix(relay): prove the optional handshake field too, not just the required ones The parser refuses a non-string `version`, `expected` and `got`, then returns the object with `endpointCredential` unproved — the most pre-auth field on the frame. It is safe today only by accident: its one reader compares it, and a non-string loses that comparison. Nothing holds that shape in place, and the next reader to put it in a log line reinstates the template-literal throw this function exists to stop. Present-but-not-a-string is now refused at the parser. Absent stays absent: a bridge presenting no credential is the common case, and refusing it would close every unauthenticated-endpoint connection. Wire-visible delta, deliberate: a peer sending a non-string credential used to get `orca-relay-handshake-credential-mismatch` and exit 43; it now gets a bare close. No first-party client can reach it — `runConnectHandshake` types the parameter `string` and omits it when falsy — and a bare close is the right answer to a frame that was malformed before any credential was checked. * fix(relay): carry the SAFETY: rationale main's casting gate now requires Main gained a `typescript/consistent-type-assertions` scan while this branch sat 432 commits behind, so every `as` the branch touches lands as a new finding. The parser is the one place the handshake shape is proved, so each cast names the check that earns it, and the hostile-frame cast in the round-trip test names the fact that it is a deliberate lie the type system cannot describe. * test(relay): annotate the hostile handshake frame instead of suppressing a cast JSON.parse answers `any`, so a typed const expresses the same deliberate lie the assertion did and the casting gate has nothing to flag. One fewer suppression. --- .../ssh/relay-protocol-backpressure.test.ts | 34 ++++++++ src/relay/protocol-backpressure.test.ts | 35 +++++++++ src/relay/protocol-handshake.test.ts | 78 +++++++++++++++++++ src/relay/protocol.ts | 64 ++++++++++++--- src/relay/relay-handshake-roundtrip.test.ts | 66 ++++++++++++++++ src/relay/relay-orca-cli-channel.ts | 13 +++- src/shared/relay-frame-decoder.ts | 22 ++++-- 7 files changed, 295 insertions(+), 17 deletions(-) diff --git a/src/main/ssh/relay-protocol-backpressure.test.ts b/src/main/ssh/relay-protocol-backpressure.test.ts index a1ee62655e8..45a869af16e 100644 --- a/src/main/ssh/relay-protocol-backpressure.test.ts +++ b/src/main/ssh/relay-protocol-backpressure.test.ts @@ -200,6 +200,40 @@ describe('FrameDecoder bounded turns', () => { expect(seen).toEqual([1, 4]) }) + // feed() runs straight from a transport data handler. A frame owner that threw on the first + // turn used to escape feed() and reach uncaughtException. The continuation path was already + // contained; the synchronous path must match it. + it('contains a frame owner that throws on the synchronous turn and reports one typed error', () => { + const seen: number[] = [] + const onError = vi.fn() + const pause = vi.fn() + const resume = vi.fn() + const decoder = new FrameDecoder( + (decoded) => { + if (decoded.id === 2) { + throw new Error('frame owner failed') + } + seen.push(decoded.id) + }, + onError, + { pause, resume } + ) + + expect(() => decoder.feed(Buffer.concat([frame(1), frame(2), frame(3)]))).not.toThrow() + + expect(seen).toEqual([1]) + expect(onError).toHaveBeenCalledExactlyOnceWith(expect.any(FrameDecoderContinuationError)) + expect(onError.mock.calls[0]?.[0]).toMatchObject({ + cause: expect.objectContaining({ message: 'frame owner failed' }) + }) + expect(decoder.drain()).toHaveLength(0) + expect(pause).not.toHaveBeenCalled() + expect(resume).not.toHaveBeenCalled() + + decoder.feed(frame(4)) + expect(seen).toEqual([1, 4]) + }) + it('keeps reads active for partial frames and incrementally discards oversized payloads', () => { const errors: Error[] = [] const seen: DecodedFrame[] = [] diff --git a/src/relay/protocol-backpressure.test.ts b/src/relay/protocol-backpressure.test.ts index fa4724ff960..f2693fe941f 100644 --- a/src/relay/protocol-backpressure.test.ts +++ b/src/relay/protocol-backpressure.test.ts @@ -200,6 +200,41 @@ describe('relay FrameDecoder bounded turns', () => { expect(seen).toEqual([1, 4]) }) + // feed() runs straight from a socket 'data' handler. A frame owner that threw on the first + // turn used to escape feed() and reach uncaughtException, which in the relay daemon means every + // PTY and agent session it holds dies with it. The continuation path was already contained. + it('contains a frame owner that throws on the synchronous turn and reports one typed error', () => { + const seen: number[] = [] + const onError = vi.fn() + const pause = vi.fn() + const resume = vi.fn() + const decoder = new FrameDecoder( + (decoded) => { + if (decoded.id === 2) { + throw new Error('frame owner failed') + } + seen.push(decoded.id) + }, + onError, + { pause, resume } + ) + + expect(() => decoder.feed(Buffer.concat([frame(1), frame(2), frame(3)]))).not.toThrow() + + expect(seen).toEqual([1]) + expect(onError).toHaveBeenCalledExactlyOnceWith(expect.any(FrameDecoderContinuationError)) + expect(onError.mock.calls[0]?.[0]).toMatchObject({ + cause: expect.objectContaining({ message: 'frame owner failed' }) + }) + // Residue after the bad frame is dropped rather than replayed, and no pause epoch is leaked. + expect(decoder.drain()).toHaveLength(0) + expect(pause).not.toHaveBeenCalled() + expect(resume).not.toHaveBeenCalled() + + decoder.feed(frame(4)) + expect(seen).toEqual([1, 4]) + }) + it('keeps reads active for partial frames and incrementally discards oversized payloads', () => { const errors: Error[] = [] const seen: DecodedFrame[] = [] diff --git a/src/relay/protocol-handshake.test.ts b/src/relay/protocol-handshake.test.ts index 822fea3e20b..4b4695877b7 100644 --- a/src/relay/protocol-handshake.test.ts +++ b/src/relay/protocol-handshake.test.ts @@ -61,6 +61,84 @@ describe('handshake framing', () => { expect(() => parseHandshakeMessage(bogus)).toThrow(/Unknown handshake type/) }) + // `type` is peer-supplied, so it can be an object whose String() conversion throws — which + // replaced the one diagnostic this refusal exists to produce with a primitive-conversion error. + it('still names the refusal when the peer type cannot be stringified', () => { + const hostile = Buffer.from(JSON.stringify({ type: { toString: 1 } })) + expect(() => parseHandshakeMessage(hostile)).toThrow(/Unknown handshake type: object/) + }) + + // The daemon logs the peer's version before any credential check, and `JSON.parse` can hand + // back a value a template literal throws on. The parser is the one place every reader shares. + it('rejects a version that is not a string on both arms that carry one', () => { + for (const type of ['orca-relay-handshake', 'orca-relay-handshake-ok']) { + for (const version of [{ toString: 1 }, 7, null, undefined, ['0.1.0']]) { + const payload = Buffer.from(JSON.stringify({ type, version })) + expect( + () => parseHandshakeMessage(payload), + `${type} version=${JSON.stringify(version)}` + ).toThrow(/Handshake field version is not a string/) + } + } + }) + + it('rejects a mismatch reply whose expected or got is not a string', () => { + const type = 'orca-relay-handshake-mismatch' + expect(() => + parseHandshakeMessage(Buffer.from(JSON.stringify({ type, expected: {}, got: 'b' }))) + ).toThrow(/Handshake field expected is not a string/) + expect(() => + parseHandshakeMessage(Buffer.from(JSON.stringify({ type, expected: 'a', got: 1 }))) + ).toThrow(/Handshake field got is not a string/) + }) + + it('rejects payloads that are not objects', () => { + for (const payload of ['null', '"orca-relay-handshake"', '42']) { + expect(() => parseHandshakeMessage(Buffer.from(payload)), payload).toThrow( + /Handshake payload is not an object/ + ) + } + }) + + // endpointCredential is the one optional field, and it is the most pre-auth thing on the frame. + // Its only reader compares it, so a non-string refuses today by inequality rather than by type — + // which is luck, not a guarantee. Prove it at the parser, where every reader shares it. + it('rejects a present endpointCredential that is not a string', () => { + for (const endpointCredential of [{ toString: 1 }, 7, null, ['secret'], true]) { + const payload = Buffer.from( + JSON.stringify({ type: 'orca-relay-handshake', version: '0.1.0', endpointCredential }) + ) + expect( + () => parseHandshakeMessage(payload), + `endpointCredential=${JSON.stringify(endpointCredential)}` + ).toThrow(/Handshake field endpointCredential is not a string/) + } + }) + + // Absent must stay absent: a bridge that legitimately presents no credential is the common case, + // and refusing it here would close every unauthenticated-endpoint connection in the fleet. + it('still accepts a handshake with no endpointCredential, and one with a string', () => { + const bare = Buffer.from(JSON.stringify({ type: 'orca-relay-handshake', version: '0.1.0' })) + expect(parseHandshakeMessage(bare)).toEqual({ type: 'orca-relay-handshake', version: '0.1.0' }) + const withCredential = Buffer.from( + JSON.stringify({ type: 'orca-relay-handshake', version: '0.1.0', endpointCredential: 'sec' }) + ) + expect(parseHandshakeMessage(withCredential)).toEqual({ + type: 'orca-relay-handshake', + version: '0.1.0', + endpointCredential: 'sec' + }) + }) + + it('still accepts a credential-mismatch reply, which carries no fields', () => { + const payload = Buffer.from( + JSON.stringify({ type: 'orca-relay-handshake-credential-mismatch' }) + ) + expect(parseHandshakeMessage(payload)).toEqual({ + type: 'orca-relay-handshake-credential-mismatch' + }) + }) + it('handshake frames use a distinct MessageType from Regular and KeepAlive', () => { expect(MessageType.Handshake).not.toBe(MessageType.Regular) expect(MessageType.Handshake).not.toBe(MessageType.KeepAlive) diff --git a/src/relay/protocol.ts b/src/relay/protocol.ts index 0f31b448f55..84656fed3cb 100644 --- a/src/relay/protocol.ts +++ b/src/relay/protocol.ts @@ -50,18 +50,62 @@ export function encodeHandshakeFrame(msg: HandshakeMessage): Buffer { return encodeFrame(MessageType.Handshake, 0, 0, payload) } +// Why the fields are checked and not just the type: this frame arrives before any credential, and +// both sides interpolate its version fields into log lines. `JSON.parse` can produce values a +// template literal throws on, so anything that reaches a reader must already be a string. +const HANDSHAKE_STRING_FIELDS: Readonly> = { + 'orca-relay-handshake': ['version'], + 'orca-relay-handshake-ok': ['version'], + 'orca-relay-handshake-mismatch': ['expected', 'got'], + 'orca-relay-handshake-credential-mismatch': [] +} + +// Optional fields are peer-supplied too, so the parser only proves the type of what it returns if +// it refuses a present-but-wrong one. `endpointCredential` survives today only because its single +// reader compares it and never interpolates it; the next reader to log it would restore the bug +// this function exists to stop. Absent stays absent — refusing that would break a bridge that +// legitimately presents no credential. +const HANDSHAKE_OPTIONAL_STRING_FIELDS: Readonly< + Record +> = { + 'orca-relay-handshake': ['endpointCredential'], + 'orca-relay-handshake-ok': [], + 'orca-relay-handshake-mismatch': [], + 'orca-relay-handshake-credential-mismatch': [] +} + export function parseHandshakeMessage(payload: Buffer): HandshakeMessage { - const msg = JSON.parse(payload.toString('utf-8')) as HandshakeMessage - const t = (msg as { type?: string }).type - if ( - t !== 'orca-relay-handshake' && - t !== 'orca-relay-handshake-ok' && - t !== 'orca-relay-handshake-mismatch' && - t !== 'orca-relay-handshake-credential-mismatch' - ) { - throw new Error(`Unknown handshake type: ${t}`) + const parsed: unknown = JSON.parse(payload.toString('utf-8')) + if (typeof parsed !== 'object' || parsed === null) { + throw new Error('Handshake payload is not an object') } - return msg + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the typeof/null guard directly above is exactly what makes this an index-able object; every read below still proves its own field. + const msg = parsed as Record + const t = msg.type + const required = + typeof t === 'string' && Object.hasOwn(HANDSHAKE_STRING_FIELDS, t) + ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: reached only when Object.hasOwn proved t is a key of this record, on the same line. + HANDSHAKE_STRING_FIELDS[t as HandshakeMessage['type']] + : null + if (required === null) { + // Why typeof and not String(t): a peer-supplied `{ "type": { "toString": 1 } }` makes String() + // itself throw "Cannot convert object to primitive value", replacing the one diagnostic this + // line exists to produce. + throw new Error(`Unknown handshake type: ${typeof t === 'string' ? t : typeof t}`) + } + for (const field of required) { + if (typeof msg[field] !== 'string') { + throw new Error(`Handshake field ${field} is not a string`) + } + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the required === null bail above already refused every t that is not one of the four keys. + for (const field of HANDSHAKE_OPTIONAL_STRING_FIELDS[t as HandshakeMessage['type']]) { + if (msg[field] !== undefined && typeof msg[field] !== 'string') { + throw new Error(`Handshake field ${field} is not a string`) + } + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this is the one place the shape is proved: the type is one of the four literals and every field the union declares has been checked to be a string. + return msg as unknown as HandshakeMessage } export const KEEPALIVE_SEND_MS = 5_000 diff --git a/src/relay/relay-handshake-roundtrip.test.ts b/src/relay/relay-handshake-roundtrip.test.ts index 0713d62295e..bfc345e8366 100644 --- a/src/relay/relay-handshake-roundtrip.test.ts +++ b/src/relay/relay-handshake-roundtrip.test.ts @@ -14,6 +14,7 @@ import { encodeJsonRpcFrame, FrameDecoder, type DecodedFrame, + type HandshakeMessage, MessageType } from './protocol' import { relayTestSocketPath } from './relay-test-socket-path' @@ -246,4 +247,69 @@ describe('handshake round-trip over a real Socket pair', () => { bridgeSock.destroy() }) + + // The daemon reads one handshake frame before any credential check, so every field on it is + // untrusted input. `JSON.parse` hands back objects a template literal cannot stringify, and the + // frame callback runs inside the decoder: a throw there used to escape the socket's data + // handler and take the daemon — and every PTY and agent session it held — down with it. + it('closes a connection whose handshake version is not a string and keeps serving', async () => { + const { accepted } = await startDaemon('0.1.0+server-version') + + const hostile = connect(sockPath) + await new Promise((r) => hostile.once('connect', () => r())) + const hostileClosed = new Promise((r) => hostile.once('close', () => r())) + // The annotation is deliberately a lie: this is the frame a hostile peer sends, and + // HandshakeMessage cannot describe it. JSON.parse answers `any`, so it needs no assertion. + const hostileFrame: HandshakeMessage = JSON.parse( + '{"type":"orca-relay-handshake","version":{"toString":1}}' + ) + hostile.write(encodeHandshakeFrame(hostileFrame)) + await hostileClosed + + const good = connect(sockPath) + await new Promise((r) => good.once('connect', () => r())) + const acceptedCb = vi.fn<(leftover: Buffer) => void>() + runConnectHandshake(good, '0.1.0+server-version', { onAccepted: acceptedCb }) + await accepted + await vi.waitFor(() => expect(acceptedCb).toHaveBeenCalledTimes(1)) + + good.destroy() + }) + + // Same class, different instance: `onAccepted` runs inside the frame callback too, so a throw + // from the accept path must cost that one connection and nothing else. + it('closes only the connection whose accept path throws', async () => { + let connections = 0 + const acceptedSockets: Socket[] = [] + server = createServer((sock) => { + trackServerSocket(sock) + connections += 1 + const failThisOne = connections === 1 + setupDaemonHandshake(sock, { + launchVersion: '0.1.0+server-version', + onAccepted: (s) => { + if (failThisOne) { + throw new Error('accept path failed') + } + acceptedSockets.push(s) + } + }) + }) + await new Promise((r) => server.listen(sockPath, () => r())) + + const first = connect(sockPath) + await new Promise((r) => first.once('connect', () => r())) + const firstClosed = new Promise((r) => first.once('close', () => r())) + runConnectHandshake(first, '0.1.0+server-version', { onAccepted: vi.fn() }) + await firstClosed + + const second = connect(sockPath) + await new Promise((r) => second.once('connect', () => r())) + const acceptedCb = vi.fn<(leftover: Buffer) => void>() + runConnectHandshake(second, '0.1.0+server-version', { onAccepted: acceptedCb }) + await vi.waitFor(() => expect(acceptedCb).toHaveBeenCalledTimes(1)) + expect(acceptedSockets).toHaveLength(1) + + second.destroy() + }) }) diff --git a/src/relay/relay-orca-cli-channel.ts b/src/relay/relay-orca-cli-channel.ts index 2afbd363fe5..1d528c63280 100644 --- a/src/relay/relay-orca-cli-channel.ts +++ b/src/relay/relay-orca-cli-channel.ts @@ -151,6 +151,17 @@ export async function runRelayOrcaCliChannel( } } + // Why an explicit error path: the decoder contains a throwing frame owner instead of letting + // it escape, so a malformed relay reply must still end this one-shot command, not park it. + const onDecodeError = (error: Error): void => { + // Why exit inside the write callback: stderr is async on pipe transports, so exiting early + // drops the only evidence this failure ever produces — the same reason relay-handshake.ts + // writes its mismatch line this way. + process.stderr.write(`[orca-cli] Relay protocol error: ${error.message}\n`, () => { + sock.destroy() + process.exit(1) + }) + } const decoder = new FrameDecoder((frame: DecodedFrame) => { if (frame.id > highestReceivedSeq) { highestReceivedSeq = frame.id @@ -194,7 +205,7 @@ export async function runRelayOrcaCliChannel( } sendPostOutput(result.postOutput) }) - }) + }, onDecodeError) const connectTimeout = setTimeout(() => { process.stderr.write(`[orca-cli] Relay connection timed out after ${CONNECT_TIMEOUT_MS}ms\n`) diff --git a/src/shared/relay-frame-decoder.ts b/src/shared/relay-frame-decoder.ts index 22a1c348185..e2b012593e3 100644 --- a/src/shared/relay-frame-decoder.ts +++ b/src/shared/relay-frame-decoder.ts @@ -143,12 +143,22 @@ export class FrameDecoder { const framed = this.buffer.take(totalLength) frames += 1 bytes += totalLength - this.onFrame({ - type: framed[0], - id: framed.readUInt32BE(1), - ack: framed.readUInt32BE(5), - payload: framed.subarray(HEADER_LENGTH, totalLength) - }) + // Why contain here and not in the caller: feed() runs straight from a socket 'data' + // handler, so a frame owner that throws on the first turn would escape as an + // uncaughtException and take the whole process — and every connection it serves — down. + // The continuation path already contains this; the synchronous path must match it, so + // one bad frame costs one connection (the owner's onError closes it), never the process. + try { + this.onFrame({ + type: framed[0], + id: framed.readUInt32BE(1), + ack: framed.readUInt32BE(5), + payload: framed.subarray(HEADER_LENGTH, totalLength) + }) + } catch (error) { + // reset() bumps the generation, which ends this turn and drops the residue. + containFrameDecoderContinuation(() => this.reset(), this.onError, error) + } } } finally { this.draining = false From edbcf68e537824ee42a388f14b966580cdc13218 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:21:53 -0700 Subject: [PATCH 44/59] fix(ssh): record the superseded-relay pass the Windows arm abandons (#20045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ssh): record the superseded-relay pass the Windows arm abandons `sweepSupersededRelayEndpoints` returned `[]` for every Windows remote host and for every failed listing without writing a line. Both returns are indistinguishable from "this host had no orphans", which is the one thing this sweep exists not to be: its own header says it makes the orphan population "visible and deliberate rather than silent". The Windows population is real. `relayEndpointForHost` hashes the version directory into the pipe name, so an app update strands the incumbent exactly as it does on POSIX, and with `--grace-time 0` that relay keeps its PTYs and agents forever. Measured on a Windows 11 host (awin): the NPFS root lists 262 named pipes from an unprivileged shell, and the count of `orca-relay-*` names goes 0 -> 1 the moment a relay binds, so the endpoints are enumerable; the repo already enumerates them for GC via `relayLivenessProbeCommand`'s `.windows-active-pipe-*` marker scan. Reclaiming them is not this change. `probeRelayEndpointIncumbent` answers `unverifiable` for every Windows path, so nothing here could be classified, let alone reaped, and nothing about the kill path moves. What changes is that an abandoned pass now leaves a trace. * fix(ssh): keep the endpoints a half-run superseded sweep already classified The Windows arm and the failed-listing arm now both leave a line. The loop between them did not: socket 1 could be fully probed and classified, and an exec on socket 2 that threw took `logSupersededRelayFindings` with it — so a half-run pass and a host with nothing to sweep produced the same silence, and socket 1's verdict was lost. Only one failure class can leave that loop, and it is the one that matters: an exec whose SSH channel never confirmed close, which may still be running remotely and which `probeRelayEndpointIncumbent` rethrows by design. Every ordinary probe failure already degrades to `unverifiable` and the pass continues — a test now pins that too, so nobody "fixes" the loop into stopping on an absence of evidence. Findings are logged before the rethrow, which propagates unchanged. The added line says how far the pass got and claims nothing about the endpoints it never reached. * fix(ssh): word the Windows sweep skip so a first install does not read as orphaned The line fired on every Windows relay launch and asserted a population: "orphans from earlier builds are neither listed nor reclaimed" reads as a finding on a machine that has never had an earlier build. The skip is what is being recorded, not a census. --- .../ssh-relay-superseded-endpoints.test.ts | 63 ++++++++++++++++++- .../ssh/ssh-relay-superseded-endpoints.ts | 61 +++++++++++++----- 2 files changed, 109 insertions(+), 15 deletions(-) diff --git a/src/main/ssh/ssh-relay-superseded-endpoints.test.ts b/src/main/ssh/ssh-relay-superseded-endpoints.test.ts index 03f7e478905..dc091b67762 100644 --- a/src/main/ssh/ssh-relay-superseded-endpoints.test.ts +++ b/src/main/ssh/ssh-relay-superseded-endpoints.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest' const execCommand = vi.fn() vi.mock('./ssh-relay-deploy-helpers', () => ({ @@ -46,6 +46,13 @@ function issuedCommands(): string[] { return execCommand.mock.calls.map((call) => String(call[1])) } +/** The `beforeEach` spy is reinstalled, not reset, so its calls survive the previous test. */ +function warnSpy(): MockInstance { + const spy = vi.spyOn(console, 'warn') + spy.mockClear() + return spy +} + beforeEach(() => { execCommand.mockReset() vi.spyOn(console, 'warn').mockImplementation(() => {}) @@ -177,8 +184,62 @@ describe('sweepSupersededRelayEndpoints', () => { await expect(sweepSupersededRelayEndpoints(CONN, HOST, SWEEP)).resolves.toEqual([]) }) + it('records the abandoned pass when the listing fails, so it reads apart from an empty host', async () => { + const warn = warnSpy() + execCommand.mockRejectedValueOnce(new Error('exec failed')) + await sweepSupersededRelayEndpoints(CONN, HOST, SWEEP) + expect(warn.mock.calls.flat().join('\n')).toContain('no pass ran: exec failed') + }) + + // Same defect as the two arms above, one level down. An ordinary probe failure degrades to + // `unverifiable` and the loop carries on, so the only way out of it mid-pass is the one case that + // matters most: an exec whose SSH channel never confirmed close, which may still be running + // remotely. That rethrows by design — and it used to throw past the log, losing socket 1's + // verdict and making a half-run pass read exactly like a host with nothing to sweep. + it('keeps the endpoints it already classified when a later probe cannot confirm termination', async () => { + const SECOND_SOCK = `${HOME}/.orca-remote/relay-0.1.0+cafebabe1234/${SOCK_NAME}` + const unconfirmed = Object.assign(new Error('channel close unconfirmed'), { + sshChannelCloseConfirmed: false + }) + const warn = warnSpy() + execCommand + .mockResolvedValueOnce(`${OLD_SOCK}\n${SECOND_SOCK}\n`) + .mockResolvedValueOnce(probe(['PRESENT=yes', 'LISTEN=unknown', 'HOLDERS_SOURCE=unavailable'])) + .mockRejectedValueOnce(unconfirmed) + + await expect(sweepSupersededRelayEndpoints(CONN, HOST, SWEEP)).rejects.toBe(unconfirmed) + + const logged = warn.mock.calls.flat().join('\n') + // Socket 1's verdict survives the abandon... + expect(logged).toContain('Superseded relay unverifiable') + expect(logged).toContain(OLD_SOCK) + // ...and the pass says how far it got, claiming nothing about the one it never reached. + expect(logged).toContain('stopped after 1 of 2 endpoints') + expect(logged).not.toContain(SECOND_SOCK) + }) + + // The loop must not stop on a probe that merely failed: that is an absence of evidence, and the + // remaining endpoints still deserve a pass. + it('carries on past an ordinary probe failure and classifies the rest', async () => { + const SECOND_SOCK = `${HOME}/.orca-remote/relay-0.1.0+cafebabe1234/${SOCK_NAME}` + execCommand + .mockResolvedValueOnce(`${OLD_SOCK}\n${SECOND_SOCK}\n`) + .mockRejectedValueOnce(new Error('probe blew up')) + .mockResolvedValueOnce(probe(['PRESENT=yes', 'LISTEN=unknown', 'HOLDERS_SOURCE=unavailable'])) + + const findings = await sweepSupersededRelayEndpoints(CONN, HOST, SWEEP) + + expect(findings.map((f) => f.outcome)).toEqual(['unverifiable', 'unverifiable']) + }) + it('does not run against Windows hosts, whose endpoints are named pipes', async () => { + const warn = warnSpy() await expect(sweepSupersededRelayEndpoints(CONN, WINDOWS_HOST, SWEEP)).resolves.toEqual([]) expect(execCommand).not.toHaveBeenCalled() + // The skip has to leave a trace: a Windows orphan is never listed and never reclaimed, and + // an empty return is otherwise indistinguishable from a host that had nothing to sweep. + const logged = warn.mock.calls.flat().join('\n') + expect(logged).toContain('Superseded relay sweep did not run') + expect(logged).toContain(CURRENT_DIR) }) }) diff --git a/src/main/ssh/ssh-relay-superseded-endpoints.ts b/src/main/ssh/ssh-relay-superseded-endpoints.ts index 4b1ad5637ef..ade50943c32 100644 --- a/src/main/ssh/ssh-relay-superseded-endpoints.ts +++ b/src/main/ssh/ssh-relay-superseded-endpoints.ts @@ -118,6 +118,17 @@ export async function sweepSupersededRelayEndpoints( options: SupersededRelaySweepOptions ): Promise { if (isWindowsRemoteHost(hostPlatform)) { + // No pass runs here: a Windows endpoint is a named pipe with no inode to stat, so the + // `$HOME` glob cannot see it, and `probeRelayEndpointIncumbent` answers `unverifiable` for + // every Windows path anyway — nothing on this host could be classified, let alone reaped. + // The population is real all the same (`relayEndpointForHost` hashes the version dir into + // the pipe name, so an update strands the incumbent exactly as it does on POSIX), and with + // `--grace-time 0` it keeps its PTYs forever. Returning silently was the whole bug: this + // sweep exists to make that population visible, and on Windows it made it invisible. + console.warn( + `[ssh-relay] Superseded relay sweep did not run (Windows named-pipe endpoints are not enumerated); ` + + `any orphan from an earlier build would be neither listed nor reclaimed: current=${options.currentRelayDir}` + ) return [] } let listing: string @@ -126,7 +137,14 @@ export async function sweepSupersededRelayEndpoints( wrapCommand: true, signal: options.signal }) - } catch { + } catch (err) { + // Same reason the Windows arm logs: an abandoned pass and an empty host are the same return + // value, and only the log tells them apart. + console.warn( + `[ssh-relay] Superseded relay listing failed; no pass ran: ${ + err instanceof Error ? err.message : String(err) + }` + ) return [] } const sockPaths = listing @@ -136,20 +154,35 @@ export async function sweepSupersededRelayEndpoints( .slice(0, MAX_SWEPT_ENDPOINTS) const findings: SupersededRelayFinding[] = [] - for (const sockPath of sockPaths) { - options.signal?.throwIfAborted() - const incumbent = await probeRelayEndpointIncumbent( - conn, - hostPlatform, - options.nodePath, - sockPath, - { signal: options.signal } + try { + for (const sockPath of sockPaths) { + options.signal?.throwIfAborted() + const incumbent = await probeRelayEndpointIncumbent( + conn, + hostPlatform, + options.nodePath, + sockPath, + { signal: options.signal } + ) + findings.push({ + sockPath, + outcome: await applySupersededRelayDecision(conn, incumbent, options), + incumbent + }) + } + } catch (err) { + // Why log before rethrowing: a probe or a reap that throws on socket 2 of N already classified + // socket 1, and those lines are the whole point of this pass. Dropping them made a half-run + // sweep read exactly like a host with nothing to sweep — the same defect the Windows arm above + // has, one level down. The throw still propagates unchanged; the caller separates + // RelayProbeCleanupUnconfirmedError from the rest. The count says how much of the pass ran, and + // claims nothing about the endpoints it never reached. + logSupersededRelayFindings(findings) + console.warn( + `[ssh-relay] Superseded relay sweep stopped after ${findings.length} of ${sockPaths.length} ` + + `endpoints; the rest were not examined: ${err instanceof Error ? err.message : String(err)}` ) - findings.push({ - sockPath, - outcome: await applySupersededRelayDecision(conn, incumbent, options), - incumbent - }) + throw err } logSupersededRelayFindings(findings) return findings From b6e039dec02abfd6ad6230d349a0134062097e4b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:22:07 -0700 Subject: [PATCH 45/59] fix(settings): read host reachability from the shared verdict (#21206) Settings > Available Hosts and the repository host-setup section render the same host from the same store entry, but this row derived its own answer from raw `entry.status`. An unverifiable probe nulls that while the transport is still up, so the row flipped to "error" and swapped Disconnect for Connect while the other surface -- which already goes through runtimeHostConnectionStateForEntry -- still showed the host as reachable. One host, two surfaces, opposite answers. A probe that did not come back is not a host that went away. --- ...time-server-row-unverifiable-host.test.tsx | 129 ++++++++++++++++++ .../settings/runtime-server-row.tsx | 16 ++- 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/settings/runtime-server-row-unverifiable-host.test.tsx diff --git a/src/renderer/src/components/settings/runtime-server-row-unverifiable-host.test.tsx b/src/renderer/src/components/settings/runtime-server-row-unverifiable-host.test.tsx new file mode 100644 index 00000000000..6a1ba57dd0e --- /dev/null +++ b/src/renderer/src/components/settings/runtime-server-row-unverifiable-host.test.tsx @@ -0,0 +1,129 @@ +// @vitest-environment happy-dom + +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { + MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION, + RUNTIME_PROTOCOL_VERSION +} from '../../../../shared/protocol-version' +import type { + RuntimeEnvironmentStatus, + RuntimeHostStatusSnapshot +} from '../../../../shared/runtime-host-status' +import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments' +import type { RuntimeStatus } from '../../../../shared/runtime-types' +import { useAppStore } from '@/store' +import { RuntimeServerRow } from './runtime-server-row' + +const ENVIRONMENT_ID = 'env-a' +const initialState = useAppStore.getInitialState() + +const environment: PublicKnownRuntimeEnvironment = { + id: ENVIRONMENT_ID, + name: 'Windows box', + createdAt: 100, + updatedAt: 100, + pairingRevision: 1, + lastUsedAt: null, + runtimeId: null, + endpoints: [{ id: 'ws-a', kind: 'websocket', label: 'WebSocket', endpoint: 'ws://x' }], + preferredEndpointId: 'ws-a' +} + +function answeredStatus(): RuntimeStatus { + return { + runtimeId: 'rt-1', + rendererGraphEpoch: 0, + graphStatus: 'ready', + authoritativeWindowId: 1, + liveTabCount: 0, + liveLeafCount: 0, + // Why real versions: an omitted protocol version is a compat block, which is its own + // disconnected verdict and would mask what this file is measuring. + runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION, + minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION + } +} + +function snapshot(patch: Partial): RuntimeHostStatusSnapshot { + return { + environmentId: ENVIRONMENT_ID, + pairingRevision: 1, + sequence: 2, + checkedAt: 2, + status: answeredStatus(), + verification: 'verified', + transport: 'ready', + ...patch + } +} + +function setEntry(entry: RuntimeEnvironmentStatus): void { + useAppStore.setState({ runtimeStatusByEnvironmentId: new Map([[ENVIRONMENT_ID, entry]]) }) +} + +function renderRow(): void { + render( + + ) +} + +beforeEach(() => { + useAppStore.setState(initialState, true) +}) + +afterEach(() => { + cleanup() + useAppStore.setState(initialState, true) +}) + +// Settings > Available Hosts and the repository host-setup section render the same host from the +// same entry. This row derived its own answer from raw `entry.status`, so a probe that did not +// come back flipped it to "error" and swapped Disconnect for Connect while the other surface, +// which already reads the shared verdict, still showed the host as reachable. +it('keeps offering Disconnect while a ready host answers an unverifiable probe', () => { + setEntry({ + status: null, + checkedAt: 2, + snapshot: snapshot({ verification: 'unavailable' }) + }) + renderRow() + + expect(screen.queryByRole('button', { name: /disconnect/i })).not.toBeNull() + expect(screen.queryByRole('button', { name: /^connect$/i })).toBeNull() +}) + +// The other direction must still work: a transport the host actually dropped is a host you +// reconnect to, and the row has to offer that. +it('offers Connect once the transport itself is down', () => { + setEntry({ + status: null, + checkedAt: 2, + snapshot: snapshot({ verification: 'unavailable', transport: 'disconnected' }) + }) + renderRow() + + expect(screen.queryByRole('button', { name: /disconnect/i })).toBeNull() +}) + +it('still offers Disconnect for a verified host', () => { + setEntry({ status: answeredStatus(), checkedAt: 2, snapshot: snapshot({}) }) + renderRow() + + expect(screen.queryByRole('button', { name: /disconnect/i })).not.toBeNull() +}) diff --git a/src/renderer/src/components/settings/runtime-server-row.tsx b/src/renderer/src/components/settings/runtime-server-row.tsx index b87b978d9ec..989e6a67d54 100644 --- a/src/renderer/src/components/settings/runtime-server-row.tsx +++ b/src/renderer/src/components/settings/runtime-server-row.tsx @@ -3,6 +3,10 @@ import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-e import type { RemoteServerUpdateEntry } from '@/runtime/remote-server-update-coordinator' import { translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' +import { + isConnectedRuntimeHostState, + runtimeHostConnectionStateForEntry +} from '@/runtime/runtime-host-connection-state' import { useAppStore } from '@/store' import { Button } from '../ui/button' import { @@ -56,15 +60,23 @@ export function RuntimeServerRow({ const runtimeStatusEntry = useAppStore((state) => state.runtimeStatusByEnvironmentId.get(environment.id) ) + // Why the shared verdict and not `entry.status`: an unverifiable probe nulls it while the + // transport is still up, and this row then read "error" and offered Connect for a host that + // RepositoryHostSetupsSection -- which already derives through this same function -- was + // showing as reachable. One host, two surfaces, opposite answers. A probe that did not come + // back is not a host that went away (docs/reference/ssh-execution-boundary.md). + const entryReachable = + runtimeStatusEntry !== undefined && + isConnectedRuntimeHostState(runtimeHostConnectionStateForEntry(runtimeStatusEntry)) const effectiveDetails = runtimeStatusEntry ? { ...(details ?? { - status: runtimeStatusEntry.status ? ('ready' as const) : ('error' as const), + status: entryReachable ? ('ready' as const) : ('error' as const), runtimeStatus: null, compatibility: null, error: null }), - status: runtimeStatusEntry.status ? ('ready' as const) : ('error' as const), + status: entryReachable ? ('ready' as const) : ('error' as const), runtimeStatus: runtimeStatusEntry.status, compatibility: runtimeStatusEntry.status ? evaluateHostDetails(runtimeStatusEntry.status) From 355757c9477b86f82b9544f44cd4432e508efc9c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:22:23 -0700 Subject: [PATCH 46/59] fix(terminal): keep the host's platform through an unverifiable probe (#21188) The platform a host runs is a fact about the host, not about whether its last probe came back. Reading `entry.status` fell through to the client's platform the moment a probe went unverifiable, so a Windows host driven from a Mac silently started resolving keystrokes and paths with POSIX conventions mid-session -- and switched back on the next successful probe. Same conversion as the four sibling reads, using the same shared reader. --- .../terminal-input-host-platform.test.ts | 55 +++++++++++++++++++ .../terminal-input-host-platform.ts | 12 ++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts b/src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts index 5455618ee3c..da672176ee8 100644 --- a/src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from 'vitest' +import type { RuntimeStatus } from '../../../../shared/runtime-types' +import type { RuntimeEnvironmentStatus } from '../../../../shared/runtime-host-status' import type { AppState } from '@/store/types' import { resolveTerminalInputHostPlatform } from './terminal-input-host-platform' @@ -18,6 +20,32 @@ function state(overrides: Partial = {}): AppState { } as AppState } +/** A Windows host that answered once and whose latest probe came back unverifiable. */ +function unverifiableWindowsHost(): RuntimeEnvironmentStatus { + const answered: RuntimeStatus = { + runtimeId: 'rt-win', + rendererGraphEpoch: 0, + graphStatus: 'ready', + authoritativeWindowId: null, + liveTabCount: 0, + liveLeafCount: 0, + hostPlatform: 'win32' + } + return { + status: null, + checkedAt: 2, + snapshot: { + environmentId: 'windows-box', + pairingRevision: 1, + sequence: 2, + checkedAt: 2, + status: answered, + verification: 'unavailable', + transport: 'ready' + } + } +} + describe('resolveTerminalInputHostPlatform', () => { it('uses a paired runtime host platform instead of the macOS client', () => { const worktreeId = 'repo::C:\\repo' @@ -280,6 +308,33 @@ describe('resolveTerminalInputHostPlatform', () => { ).toBe('win32') }) + // A probe that did not come back says nothing about which OS the host runs. Falling through to + // the client's platform re-points every keystroke and every path at the wrong conventions -- + // a Windows host driven from a Mac silently starts speaking POSIX mid-session. + it('keeps the Windows host platform while its probe is unverifiable', () => { + const worktreeId = 'repo::C:\\repo' + expect( + resolveTerminalInputHostPlatform({ + clientPlatform: 'darwin', + state: state({ + repos: [ + { + id: 'repo', + path: 'C:\\repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0, + executionHostId: 'runtime:windows-box' + } + ], + runtimeStatusByEnvironmentId: new Map([['windows-box', unverifiableWindowsHost()]]) + }), + worktreeId, + transport: null + }) + ).toBe('win32') + }) + it('keeps the client platform for local terminals', () => { expect( resolveTerminalInputHostPlatform({ diff --git a/src/renderer/src/components/terminal-pane/terminal-input-host-platform.ts b/src/renderer/src/components/terminal-pane/terminal-input-host-platform.ts index 5758f41604a..2faaad7a117 100644 --- a/src/renderer/src/components/terminal-pane/terminal-input-host-platform.ts +++ b/src/renderer/src/components/terminal-pane/terminal-input-host-platform.ts @@ -1,4 +1,5 @@ import { parseExecutionHostId } from '../../../../shared/execution-host' +import { lastVerifiedRuntimeStatus } from '../../../../shared/runtime-host-status' import { isWslUncPath } from '../../../../shared/wsl-paths' import { getConnectionIdFromState } from '@/lib/connection-context' import { @@ -71,9 +72,12 @@ export function resolveTerminalInputHostPlatform(args: { ) } if (runtimeEnvironmentId) { + // Why last-verified: the host's platform is a fact about the host, and falling back to the + // client's silently re-points every keystroke and path at the wrong conventions -- a Windows + // host driven from a Mac. See docs/reference/ssh-execution-boundary.md. return ( - args.state.runtimeStatusByEnvironmentId.get(runtimeEnvironmentId)?.status?.hostPlatform ?? - args.clientPlatform + lastVerifiedRuntimeStatus(args.state.runtimeStatusByEnvironmentId.get(runtimeEnvironmentId)) + ?.hostPlatform ?? args.clientPlatform ) } const localSessionMetadata = args.transport?.getLocalSessionMetadata?.() @@ -95,8 +99,8 @@ export function resolveTerminalInputHostPlatform(args: { } if (host?.kind === 'runtime') { return ( - args.state.runtimeStatusByEnvironmentId.get(host.environmentId)?.status?.hostPlatform ?? - args.clientPlatform + lastVerifiedRuntimeStatus(args.state.runtimeStatusByEnvironmentId.get(host.environmentId)) + ?.hostPlatform ?? args.clientPlatform ) } return args.clientPlatform From a61119ceb0f21ba5a9b5a6fc7e08d94d295824de Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:22:39 -0700 Subject: [PATCH 47/59] refactor(runtime): name the four answers a host probe can give (#21207) The renderer expressed every non-answer as one nullable `status`, so a probe in flight, a probe that failed, a host that refused us and a retired pairing all reached readers as the same `null` -- and readers spent that `null` on decisions of very different weight, including destructive ones. `RuntimeHostContact` names the four. Nothing changes yet: the connection-state derivation is rewritten on top of it and a 384-case parity table asserts the result is identical to a frozen copy of the old one on every combination of verification, transport, retired, answered and remote-control state. --- docs/reference/ssh-execution-boundary.md | 27 ++ .../runtime/runtime-host-connection-state.ts | 30 ++- .../runtime-host-contact-parity.test.ts | 239 ++++++++++++++++++ src/shared/runtime-host-contact.ts | 118 +++++++++ 4 files changed, 401 insertions(+), 13 deletions(-) create mode 100644 src/renderer/src/runtime/runtime-host-contact-parity.test.ts create mode 100644 src/shared/runtime-host-contact.ts diff --git a/docs/reference/ssh-execution-boundary.md b/docs/reference/ssh-execution-boundary.md index 88a4a3c0a0e..a113a5e6f29 100644 --- a/docs/reference/ssh-execution-boundary.md +++ b/docs/reference/ssh-execution-boundary.md @@ -72,6 +72,33 @@ A verdict needs evidence from the host that owns the process. Apply these tests Anything short of positive host evidence is `unverifiable`. Reporting it as `exited` is the error this document exists to prevent: it orphans live work and can cold-start a duplicate over the same worktree. +## Host contact is a different question from process liveness + +The `live` / `unverifiable` / `exited` triple above answers one question: is this PTY running. It has +no synonyms, and nothing below adds any. + +A second, narrower question — can we currently reach the host at all, and what is its last answer +worth — is answered by `RuntimeHostContact` (`src/shared/runtime-host-contact.ts`), whose arms are +`live` / `unverifiable` / `refused` / `retired`. These are **not** extra process verdicts and must +never be mapped onto one: + +- `refused` is the host answering and turning us away — unauthorized, a protocol mismatch, a status + method it does not implement. That is positive evidence about the *connection*, and it says + nothing whatever about whether the host's PTYs are running. They almost certainly still are. +- `retired` is the pairing being ended by explicit user action. Same point: the client stops having + a route, the remote work is unaffected. + +Both are reasons to stop *trusting a cached answer*, never reasons to report a process `exited`. A +reader that needs a process verdict must still get it from the host that owns the process, by the +tests above. + +Why the extra arms exist at all: the renderer previously expressed every non-answer as one nullable +`status`, so a probe in flight, a probe that failed, a host that refused us and a retired pairing +all reached readers as the same `null` — and readers spent that `null` on decisions of very +different weight, including destructive ones. Folding `refused` and `retired` back into +`unverifiable` to match this document's triple would recreate exactly that collapse. The vocabularies +are deliberately separate because the questions are. + ## Deciding a remote pane is idle The orphan-PTY sweep is the one flow that turns an observation into a SIGKILL, so its idleness evidence has to be measured against the same thing the signal reaches. It is not the terminal. diff --git a/src/renderer/src/runtime/runtime-host-connection-state.ts b/src/renderer/src/runtime/runtime-host-connection-state.ts index 21e386857e9..4e5b99b916a 100644 --- a/src/renderer/src/runtime/runtime-host-connection-state.ts +++ b/src/renderer/src/runtime/runtime-host-connection-state.ts @@ -1,7 +1,5 @@ -import { - isRuntimeHostContactRevoked, - type RuntimeHostStatusSnapshot -} from '../../../shared/runtime-host-status' +import { runtimeHostContactFromSnapshot } from '../../../shared/runtime-host-contact' +import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status' import type { RuntimeStatus } from '../../../shared/runtime-types' import { isRuntimeWorkspaceWindowClosed } from '../../../shared/runtime-workspace-window-availability' @@ -123,17 +121,23 @@ export function runtimeHostConnectionStateForEntry( ): RuntimeHostConnectionState { const snapshot = entry?.snapshot if (snapshot) { - if (isRuntimeHostContactRevoked(entry)) { + // Why the contact and not the snapshot fields: these four branches were the only place that + // knew a non-verified probe has kinds, and every other reader had to re-derive them or guess. + // Naming them once means the next reader picks an arm instead of re-reading a null. + const contact = runtimeHostContactFromSnapshot(snapshot, entry?.status ?? null) + if (contact.verdict === 'retired' || contact.verdict === 'refused') { return 'disconnected' } - if (snapshot.transport === 'disconnected') { - return 'reconnecting' - } - if (snapshot.verification === 'checking' && !entry?.status) { - return 'checking' - } - if (snapshot.transport === 'ready' && snapshot.verification !== 'verified') { - return 'runtime-unavailable' + if (contact.verdict === 'unverifiable') { + if (contact.reason === 'transport-down') { + return 'reconnecting' + } + if (contact.reason === 'checking') { + return 'checking' + } + if (contact.reason === 'probe-failed') { + return 'runtime-unavailable' + } } } return runtimeHostConnectionState({ diff --git a/src/renderer/src/runtime/runtime-host-contact-parity.test.ts b/src/renderer/src/runtime/runtime-host-contact-parity.test.ts new file mode 100644 index 00000000000..a24e963a1d9 --- /dev/null +++ b/src/renderer/src/runtime/runtime-host-contact-parity.test.ts @@ -0,0 +1,239 @@ +import { describe, expect, it } from 'vitest' +import { + isRuntimeHostContactRevoked, + type RuntimeHostStatusSnapshot +} from '../../../shared/runtime-host-status' +import { + isRuntimeHostContactRevokedVerdict, + lastRuntimeHostAnswer, + liveRuntimeHostStatus, + runtimeHostContactFromSnapshot +} from '../../../shared/runtime-host-contact' +import type { RuntimeStatus } from '../../../shared/runtime-types' +import { + runtimeHostConnectionState, + runtimeHostConnectionStateForEntry, + type RuntimeHostConnectionState +} from './runtime-host-connection-state' + +// This file exists to prove the contact introduced here changes nothing. It carries a frozen copy +// of the derivation as it stood before, and asserts the shipping one agrees with it on every +// combination of the inputs it reads. A behaviour change would have to survive the whole +// cross-product to go unnoticed, which is a much harder thing to do by accident than to argue. + +type Entry = { + status: RuntimeStatus | null + remoteControl?: RuntimeStatus['remoteControl'] | null + snapshot?: RuntimeHostStatusSnapshot +} + +/** The derivation exactly as it read before `RuntimeHostContact` existed. Do not refactor. */ +function legacyRuntimeHostConnectionStateForEntry( + entry: Entry | null | undefined +): RuntimeHostConnectionState { + const snapshot = entry?.snapshot + if (snapshot) { + if (snapshot.retired || snapshot.verification === 'blocked') { + return 'disconnected' + } + if (snapshot.transport === 'disconnected') { + return 'reconnecting' + } + if (snapshot.verification === 'checking' && !entry?.status) { + return 'checking' + } + if (snapshot.transport === 'ready' && snapshot.verification !== 'verified') { + return 'runtime-unavailable' + } + } + return runtimeHostConnectionState({ + hasStatusEntry: Boolean(entry), + status: entry?.status ?? null, + ...(snapshot?.transport === 'connecting' ? { transportStatus: 'checking' as const } : {}), + remoteControl: entry?.remoteControl ?? entry?.status?.remoteControl ?? null + }) +} + +const VERIFICATIONS = ['checking', 'verified', 'unavailable', 'blocked'] as const +const TRANSPORTS = ['unknown', 'connecting', 'ready', 'disconnected'] as const +const RETIRED = [false, true] as const +const REMOTE_CONTROL_STATES = [ + undefined, + 'ready', + 'awaiting_ready', + 'awaiting_authenticated', + 'reconnecting', + 'closed' +] as const + +function makeStatus(overrides: Partial = {}): RuntimeStatus { + return { + runtimeId: 'rt-1', + rendererGraphEpoch: 0, + graphStatus: 'ready', + authoritativeWindowId: 1, + liveTabCount: 0, + liveLeafCount: 0, + ...overrides + } +} + +function makeRemoteControl( + state: Exclude<(typeof REMOTE_CONTROL_STATES)[number], undefined> +): NonNullable { + return { + state, + pendingRequestCount: 0, + subscriptionCount: 0, + reconnectAttempt: 0, + lastConnectedAt: null, + lastClose: null, + lastError: null + } +} + +function makeSnapshot( + verification: (typeof VERIFICATIONS)[number], + transport: (typeof TRANSPORTS)[number], + retired: boolean, + answered: RuntimeStatus | null +): RuntimeHostStatusSnapshot { + return { + environmentId: 'env-a', + pairingRevision: 1, + sequence: 1, + checkedAt: 1, + status: answered, + verification, + transport, + ...(retired ? { retired: true as const } : {}) + } +} + +/** Every entry shape the derivation can distinguish: 4 x 4 x 2, across each status/diagnostic. */ +function* everySnapshotEntry(): Generator<{ label: string; entry: Entry }> { + for (const verification of VERIFICATIONS) { + for (const transport of TRANSPORTS) { + for (const retired of RETIRED) { + for (const answered of [null, makeStatus()] as const) { + for (const remoteControlState of REMOTE_CONTROL_STATES) { + // The store nulls `status` for anything but a verified, unretired probe, so the two + // reachable pairings are the ones enumerated here rather than a free cross-product. + const entryStatus = verification === 'verified' && !retired ? answered : null + const remoteControl = remoteControlState + ? makeRemoteControl(remoteControlState) + : undefined + yield { + label: `${verification}/${transport}/retired=${retired}/answered=${answered !== null}/rc=${remoteControlState ?? 'none'}`, + entry: { + status: entryStatus, + ...(remoteControl ? { remoteControl } : {}), + snapshot: makeSnapshot(verification, transport, retired, answered) + } + } + } + } + } + } + } +} + +describe('the host contact changes no verdict', () => { + it('agrees with the frozen derivation on every snapshot combination', () => { + const cases = [...everySnapshotEntry()] + // Guard against the enumeration silently collapsing: 4 x 4 x 2 x 2 x 6. + expect(cases).toHaveLength(384) + const disagreements = cases + .map(({ label, entry }) => ({ + label, + now: runtimeHostConnectionStateForEntry(entry), + before: legacyRuntimeHostConnectionStateForEntry(entry) + })) + .filter(({ now, before }) => now !== before) + expect(disagreements).toEqual([]) + }) + + it('agrees for entries that carry no snapshot at all', () => { + const entries: (Entry | null | undefined)[] = [ + null, + undefined, + { status: null }, + { status: makeStatus() }, + { status: null, remoteControl: makeRemoteControl('closed') }, + { status: null, remoteControl: makeRemoteControl('ready') }, + { status: makeStatus({ remoteControl: makeRemoteControl('reconnecting') }) } + ] + for (const entry of entries) { + expect(runtimeHostConnectionStateForEntry(entry)).toBe( + legacyRuntimeHostConnectionStateForEntry(entry) + ) + } + }) + + it('keeps the revoked predicate and the contact verdict in step', () => { + for (const { label, entry } of everySnapshotEntry()) { + expect( + isRuntimeHostContactRevokedVerdict( + runtimeHostContactFromSnapshot(entry.snapshot!, entry.status) + ), + label + ).toBe(isRuntimeHostContactRevoked(entry)) + } + }) +}) + +describe('the contact separates what the host said from what it is worth', () => { + it('retains the host answer through every non-live verdict', () => { + const answered = makeStatus() + for (const [verification, transport, retired] of [ + ['unavailable', 'ready', false], + ['checking', 'connecting', false], + ['unavailable', 'disconnected', false], + ['blocked', 'ready', false], + ['verified', 'ready', true] + ] as const) { + const contact = runtimeHostContactFromSnapshot( + makeSnapshot(verification, transport, retired, answered), + null + ) + expect(contact.verdict, `${verification}/${transport}`).not.toBe('live') + // The fact the host gave us survives; only its currency is in question. + expect(lastRuntimeHostAnswer(contact)).toBe(answered) + expect(liveRuntimeHostStatus(contact)).toBeNull() + } + }) + + it('reports a verified probe as live and nothing else', () => { + const answered = makeStatus() + const contact = runtimeHostContactFromSnapshot( + makeSnapshot('verified', 'ready', false, answered), + answered + ) + expect(contact.verdict).toBe('live') + expect(liveRuntimeHostStatus(contact)).toBe(answered) + expect(lastRuntimeHostAnswer(contact)).toBe(answered) + }) + + it('tells a host that was never reached apart from a handshake in flight', () => { + // These collapsed into one `null` before, and they want opposite affordances: one should + // offer Connect, the other should not. + expect( + runtimeHostContactFromSnapshot(makeSnapshot('unavailable', 'unknown', false, null), null) + ).toEqual({ verdict: 'unverifiable', reason: 'never-asked', lastAnswer: null }) + expect( + runtimeHostContactFromSnapshot(makeSnapshot('unavailable', 'connecting', false, null), null) + ).toEqual({ verdict: 'unverifiable', reason: 'transport-connecting', lastAnswer: null }) + }) + + it('tells a refused host apart from a retired pairing', () => { + const answered = makeStatus() + expect( + runtimeHostContactFromSnapshot(makeSnapshot('blocked', 'ready', false, answered), null) + .verdict + ).toBe('refused') + expect( + runtimeHostContactFromSnapshot(makeSnapshot('verified', 'ready', true, answered), null) + .verdict + ).toBe('retired') + }) +}) diff --git a/src/shared/runtime-host-contact.ts b/src/shared/runtime-host-contact.ts new file mode 100644 index 00000000000..84b3dcd0171 --- /dev/null +++ b/src/shared/runtime-host-contact.ts @@ -0,0 +1,118 @@ +import type { RuntimeHostStatusSnapshot } from './runtime-host-status' +import type { RuntimeStatus } from './runtime-types' + +/** + * What a host's last probe is worth, kept apart from what the host actually said. + * + * The store's `status` field answers both questions with one nullable value, so a probe still in + * flight, a probe that failed, a host that refused us and a pairing that was retired all arrive at + * a reader as the same `null`. Readers then spend that `null` on decisions of very different + * weight. This names the four answers so the decision happens where the evidence is understood. + * + * `unverifiable` is never `exited` (docs/reference/ssh-execution-boundary.md). `refused` and + * `retired` are the only arms carrying positive evidence, and they are separate because they + * differ in kind: one is the host turning us away, the other is the pairing being ended. + */ +export type RuntimeHostContact = + | { verdict: 'live'; status: RuntimeStatus } + | { + verdict: 'unverifiable' + reason: RuntimeHostContactUnverifiableReason + lastAnswer: RuntimeStatus | null + } + | { verdict: 'refused'; lastAnswer: RuntimeStatus | null } + | { verdict: 'retired'; lastAnswer: RuntimeStatus | null } + +/** + * Why the host's current state is unknown. `never-asked` is the absence of any transport attempt, + * which is where an unreachable paired host permanently sits — distinct from a handshake in + * flight, and the reason it must stay actionable rather than spin. + */ +export type RuntimeHostContactUnverifiableReason = + | 'never-asked' + | 'checking' + | 'probe-failed' + | 'transport-connecting' + | 'transport-down' + +/** + * The host's last answer whatever the verdict, for facts that do not expire — its build's + * capabilities, its platform, its runtime id. Returns null only when the host never answered. + */ +export function lastRuntimeHostAnswer(contact: RuntimeHostContact): RuntimeStatus | null { + return contact.verdict === 'live' ? contact.status : contact.lastAnswer +} + +/** The answer only while it is current, for decisions that must not act on a stale fact. */ +export function liveRuntimeHostStatus(contact: RuntimeHostContact): RuntimeStatus | null { + return contact.verdict === 'live' ? contact.status : null +} + +/** True only for the host's own terminal verdicts — the one state that may withdraw a fact. */ +export function isRuntimeHostContactRevokedVerdict(contact: RuntimeHostContact): boolean { + return contact.verdict === 'refused' || contact.verdict === 'retired' +} + +/** + * Why the order matters: it is the order the connection-state derivation already used, and the + * parity suite pins every combination against it. Transport loss outranks a probe in flight + * because a dead socket explains the silence; a ready transport with a failed probe is the host + * being unreachable at the runtime layer, not at the network layer. + */ +export function runtimeHostContactFromSnapshot( + snapshot: RuntimeHostStatusSnapshot, + entryStatus: RuntimeStatus | null = snapshot.status +): RuntimeHostContact { + const lastAnswer = snapshot.status + if (snapshot.retired) { + return { verdict: 'retired', lastAnswer } + } + if (snapshot.verification === 'blocked') { + return { verdict: 'refused', lastAnswer } + } + if (snapshot.transport === 'disconnected') { + return { verdict: 'unverifiable', reason: 'transport-down', lastAnswer } + } + if (snapshot.verification === 'checking' && !entryStatus) { + return { verdict: 'unverifiable', reason: 'checking', lastAnswer } + } + if (snapshot.transport === 'ready' && snapshot.verification !== 'verified') { + return { verdict: 'unverifiable', reason: 'probe-failed', lastAnswer } + } + if (snapshot.verification === 'verified' && entryStatus) { + return { verdict: 'live', status: entryStatus } + } + if (snapshot.transport === 'connecting') { + return { verdict: 'unverifiable', reason: 'transport-connecting', lastAnswer } + } + return { verdict: 'unverifiable', reason: 'never-asked', lastAnswer } +} + +/** + * The contact for a recorded entry. A stored `contact` wins so a writer can state one the + * snapshot cannot express — a probe that threw before any snapshot existed, say — and the + * snapshot derivation is the fallback while writers are still being converted. + */ +export function runtimeHostContactForEntry( + entry: + | { + status: RuntimeStatus | null + contact?: RuntimeHostContact + snapshot?: RuntimeHostStatusSnapshot + } + | null + | undefined +): RuntimeHostContact { + if (!entry) { + return { verdict: 'unverifiable', reason: 'never-asked', lastAnswer: null } + } + if (entry.contact) { + return entry.contact + } + if (entry.snapshot) { + return runtimeHostContactFromSnapshot(entry.snapshot, entry.status) + } + return entry.status + ? { verdict: 'live', status: entry.status } + : { verdict: 'unverifiable', reason: 'probe-failed', lastAnswer: null } +} From 82ca89124b8d42d604ef15ee4fe5596926bf7a17 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:34:42 -0400 Subject: [PATCH 48/59] fix(lint): exempt the descendant-sweep test shim from the module-mocking gate (#21362) #20642 and #20645 added src/main/daemon/mock-descendant-sweep.ts and src/relay/mock-descendant-sweep.ts: test-only side-effect modules whose whole body is one vi.mock, imported by 60 suites so mock PTY PIDs never reach the host process table. Their CI ran before the anti-slop gate landed, so main now fails `oxlint --config config/oxlint-anti-slop.json` on every PR's merge ref. File-scoped exemption, like the others in this config, because the root lint scan does not load the plugin and an inline directive would read back as unused. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- config/oxlint-anti-slop.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index bba8588d949..7d379a9df4e 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -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. From 660969d1919e49016c8f2aba01d60242008d0db4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:38:25 +0000 Subject: [PATCH 49/59] Update README downloads badge --- docs/assets/readme-downloads.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index 766bd1e4cad..20fd5d9c2f2 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 61m + + downloads: 62m @@ -15,7 +15,7 @@ downloads downloads - 61m - 61m + 62m + 62m From 07e8c851b8b03651468459d9a63c285329e6e105 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:39:41 -0700 Subject: [PATCH 50/59] fix(editor): evict stale mirrored file tabs (#21363) --- .../editor/useEditorPanelContentState.ts | 3 +- .../useEditorPanelFileLoadRetry.test.tsx | 32 +++++++++++++++++++ .../editor/useEditorPanelFileLoadRetry.ts | 19 +++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/editor/useEditorPanelContentState.ts b/src/renderer/src/components/editor/useEditorPanelContentState.ts index ec1a0fc6b1b..93c33d0961c 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentState.ts +++ b/src/renderer/src/components/editor/useEditorPanelContentState.ts @@ -1,6 +1,6 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react' import type { OpenFile } from '@/store/slices/editor' -import type { useAppStore } from '@/store' +import { useAppStore } from '@/store' import type { DiffContent, FileContent } from './editor-panel-content-types' import { useEditorPanelExternalContentEvents, @@ -194,6 +194,7 @@ export function useEditorPanelContentState({ fileLoadRetryAttemptsRef, loadFileContent, openFilesRef, + closeFile: useAppStore.getState().closeFile, setFileContents }) diff --git a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx index 6a29017d654..fe40142c645 100644 --- a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx +++ b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx @@ -49,6 +49,7 @@ function Harness({ attemptsRef, isVisible = true, loadFileContent, + closeFile = vi.fn(), setFileContents }: { file: OpenFile @@ -56,6 +57,7 @@ function Harness({ attemptsRef: { current: Record } isVisible?: boolean loadFileContent: (filePath: string, id: string) => Promise + closeFile?: (fileId: string) => void setFileContents: ( updater: (prev: Record) => Record ) => void @@ -66,6 +68,7 @@ function Harness({ fileLoadRetryAttemptsRef: attemptsRef, loadFileContent: loadFileContent as never, openFilesRef: { current: [file] }, + closeFile, setFileContents: setFileContents as never }) return null @@ -103,6 +106,35 @@ describe('useEditorPanelFileLoadRetry — owner-not-ready bounding (#6648)', () expect(shouldRetryFileLoadError('Access denied: outside allowed directories')).toBe(false) }) + it('evicts a mirrored tab after selector resolution stays missing', () => { + const file = makeFile({ mirroredFromRuntimeSession: true }) + const attemptsRef = { current: { [file.id]: 3 } } + const closeFile = vi.fn() + const fileContents: Record = { + [file.id]: { content: '', isBinary: false, loadError: 'selector_not_found' } + } + + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => { + root?.render( + undefined)} + closeFile={closeFile} + setFileContents={(updater) => { + updater(fileContents) + }} + /> + ) + }) + + expect(closeFile).toHaveBeenCalledWith(file.id) + }) + it('does not spend retry budget when hiding cancels a pending retry', () => { setTimeoutSpy.mockRestore() setTimeoutSpy = vi.spyOn(window, 'setTimeout') diff --git a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts index 9fb96ad80aa..57a9483218e 100644 --- a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts +++ b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts @@ -7,6 +7,7 @@ import { } from './editor-panel-content-types' const FILE_LOAD_RETRY_DELAYS_MS = [250, 1000, 2500] +const noopCloseFile = (): void => {} // Why: a remote host can take a while to finish connecting. The owner-not-ready // check is a pure local store read (it throws before any network call until the // SSH repo hydrates), so poll it at a steady cadence — but cap the wait so a @@ -30,9 +31,14 @@ type UseEditorPanelFileLoadRetryParams = { relativePath?: string ) => Promise openFilesRef: MutableRefObject + closeFile?: (fileId: string) => void setFileContents: Dispatch>> } +function isSelectorNotFoundError(message: string): boolean { + return message.trim().toLowerCase() === 'selector_not_found' +} + export function shouldRetryFileLoadError(message: string): boolean { // Terminal: the owner-not-ready budget is spent; only an explicit Retry should // restart it, never the automatic backoff. @@ -54,6 +60,7 @@ export function useEditorPanelFileLoadRetry({ fileLoadRetryAttemptsRef, loadFileContent, openFilesRef, + closeFile = noopCloseFile, setFileContents }: UseEditorPanelFileLoadRetryParams): void { const activeFileLoadRetryId = activeFile?.id ?? null @@ -75,6 +82,16 @@ export function useEditorPanelFileLoadRetry({ ? OWNER_NOT_READY_RETRY_LIMIT : FILE_LOAD_RETRY_DELAYS_MS.length if (retryCount >= retryLimit) { + if ( + !ownerNotReady && + isSelectorNotFoundError(activeFileLoadError) && + activeFile?.mirroredFromRuntimeSession === true + ) { + // A host-mirrored file whose worktree stays unresolvable after the normal + // read retries is stale; evict it before snapshots can select it again. + closeFile(activeFileLoadRetryId) + return + } // Why: the remote host never finished connecting. Replace the transient // "still connecting" text with a truthful terminal message so it does not // look like it is still retrying; Retry starts a fresh budget (#6648). @@ -126,6 +143,8 @@ export function useEditorPanelFileLoadRetry({ }, [ activeFileLoadRetryId, activeFileLoadError, + activeFile?.mirroredFromRuntimeSession, + closeFile, fileLoadRetryAttemptsRef, loadFileContent, openFilesRef, From 4b4ee040df75ea1c3f311347ff8c45cc9881ec07 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:50:15 -0700 Subject: [PATCH 51/59] perf(relay): index client request aborts instead of scanning every controller (#20052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(relay): measure per-connection teardown and hot-path costs by counting Both suites replace a would-be duration with the structural fact the duration was a proxy for, so neither depends on machine load. The census pins that attach/publish/detach churn returns every per-connection container to baseline, and asserts the containers actually filled first so a green cannot come from a probe that never loaded them. It also pins the one container with no per-client teardown: a publication-ledger entry is reclaimed only by its own lease, never by closeClient. The operation counts pin that notifyLegacyCapacity costs one ledger lookup per active client, that a broadcast costs a fixed number per subscriber, and that abortClient enumerates every controller rather than the target client's -- which is what makes a full client churn quadratic. * perf(relay): index client request aborts instead of scanning every controller abortClient runs on every closeClient and every setWrite. Under the flat map keyed `${clientId}:${requestId}` it had to walk every controller in the relay to find one client's, so a full churn of N clients each holding K in-flight requests cost K*N*(N+1)/2 key visits: measured 50 -> 5,100, 100 -> 20,200, 200 -> 80,400, 400 -> 320,800, exactly 4x per doubling. Do not "optimise" this back to a scan with an early break. It cannot work: the matching keys are scattered through the map, so any correct loop still visits every entry before it can know it is done. Only an index makes teardown proportional to what the client owns. `create` now returns an opaque handle carrying the owner, so a release finds its bucket without parsing a composite string key, and no call site changes. Also stop building the low-water key array eagerly. `belowLowWater` decides on the aggregate ceiling first and returns without reading the keys, but the caller had already allocated an N-element array and N template strings to pass them -- paying most in the loaded case, which is when that short-circuit fires. It takes a thunk now. The hot-path test becomes a guard rather than a characterisation: it asserts a teardown visits only the target client's K controllers and never enumerates the client index at all, since enumerating it is the old scan. Verified by mutation: restoring the scan shape fails it with "expected 40 to be +0". It asserts the maps really hold 160 controllers first, so it cannot pass by never filling them. * test(relay): make the capacity-thunk guard fail when the thunk is removed The operation-count test measured an idle dispatcher, where the aggregate ceiling never short-circuits, so every key is read whichever call shape is used. Reverting the thunk left all five assertions green -- it guarded nothing it claimed to. Adds the loaded arm, where the ceiling answers first and the saving exists, and asserts the client index is not enumerated at all. Reverting the thunk now fails it with `expected 50 to be +0`. Drops the ledger-retention case: it asserted a stranded entry SURVIVES close, so it pinned a capacity leak as a contract and would have broken whoever fixed it. It also used a key no client-keyed reclamation could match, and touched nothing this branch changes. The churn census already proves normal closes settle every entry; the gap is recorded there as a gap. * test(relay): carry the SAFETY: rationale main's casting gate now requires Not introduced here: main gained a `typescript/consistent-type-assertions` scan while this branch sat 432 commits behind, and every `as` in the two probe files this branch adds is new relative to main, so all 11 land as new findings. Verified by running the gate on this branch with and without my earlier test commit — 11 either way. Both files reach past `protected` to count containers, which is the measurement; each cast now carries the line-specific rationale AGENTS.md mandates. * test(relay): put the countingIterator SAFETY: directive on the line oxlint flags The diagnostic points at the `return {` that opens the object literal, not at the `} as IterableIterator` that closes it, so disable-next-line has to sit above the statement. * test(relay): type countingIterator as MapIterator and drop two suppressions The wrapper only ever receives a Map iterator, so declaring that removes the cast at both call sites; one irreducible cast stays on the object literal, which cannot satisfy MapIterator's full surface. Three suppressions become one. * fix(relay): key the abort index by the id's string form so a string id can still be cancelled The flat map's template key folded a request id of 7 and "7" onto one entry; keying the raw value split them, so rpc.cancel (which coerces through Number) missed a string-id request. Restore the coercion at the index. --- src/relay/client-request-aborts.ts | 78 +++++-- src/relay/dispatcher-capacity-signals.ts | 4 +- ...cher-per-connection-state-baseline.test.ts | 107 +++++++++ .../dispatcher-rpc-cancel-id-coercion.test.ts | 43 ++++ src/relay/legacy-relay-publication-ledger.ts | 11 +- .../relay-hot-path-operation-counts.test.ts | 205 ++++++++++++++++++ 6 files changed, 422 insertions(+), 26 deletions(-) create mode 100644 src/relay/dispatcher-per-connection-state-baseline.test.ts create mode 100644 src/relay/dispatcher-rpc-cancel-id-coercion.test.ts create mode 100644 src/relay/relay-hot-path-operation-counts.test.ts diff --git a/src/relay/client-request-aborts.ts b/src/relay/client-request-aborts.ts index 12d44363d0d..c896fb446b4 100644 --- a/src/relay/client-request-aborts.ts +++ b/src/relay/client-request-aborts.ts @@ -1,40 +1,74 @@ -export class ClientRequestAborts { - private readonly controllers = new Map() +/** Opaque handle returned by `create`, so a release needs no string parsing to find its owner. */ +export type ClientRequestAbortHandle = { + readonly clientId: number + readonly requestId: number +} - create(clientId: number, requestId: number): { key: string; controller: AbortController } { - const key = this.key(clientId, requestId) +export class ClientRequestAborts { + // Why indexed by client instead of one flat map under composite `${clientId}:${requestId}` keys: + // abortClient runs on every closeClient and every setWrite, and against a flat map it had to scan + // every entry to find one client's. A scan with an early break cannot fix that -- the matching + // keys are scattered through the map, so any correct loop still visits every entry, which made a + // full churn of N clients cost K*N*(N+1)/2 visits. Only an index makes a teardown proportional to + // what that client actually owns. + // + // Why the inner key is a string: the codec only checks `jsonrpc === '2.0'`, so a request `id` can + // arrive as `"7"` while `rpc.cancel` coerces its `id` through `Number(...)` and looks up `7`. The + // flat map's template key folded both onto `"7"`; keying the raw value would file them in + // different buckets and silently drop the cancel. `String(...)` is the template literal's coercion. + private readonly byClient = new Map>() + + create( + clientId: number, + requestId: number + ): { key: ClientRequestAbortHandle; controller: AbortController } { const controller = new AbortController() - this.controllers.set(key, controller) - return { key, controller } + let requests = this.byClient.get(clientId) + if (!requests) { + requests = new Map() + this.byClient.set(clientId, requests) + } + requests.set(String(requestId), controller) + return { key: { clientId, requestId }, controller } } get(clientId: number, requestId: number): AbortController | undefined { - return this.controllers.get(this.key(clientId, requestId)) + return this.byClient.get(clientId)?.get(String(requestId)) } - delete(key: string): void { - this.controllers.delete(key) + delete(key: ClientRequestAbortHandle): void { + const requests = this.byClient.get(key.clientId) + if (!requests) { + return + } + requests.delete(String(key.requestId)) + // Why drop the empty bucket: otherwise a churned client leaves an entry behind for the life of + // the relay, which is the retention the index exists to avoid. + if (requests.size === 0) { + this.byClient.delete(key.clientId) + } } abortClient(clientId: number): void { - const prefix = `${clientId}:` - for (const [key, controller] of this.controllers) { - if (!key.startsWith(prefix)) { - continue - } + const requests = this.byClient.get(clientId) + if (!requests) { + return + } + // Unlink before aborting: an abort listener that reaches back in must not see a half-emptied + // bucket, and the whole bucket is going regardless. + this.byClient.delete(clientId) + for (const controller of requests.values()) { controller.abort() - this.controllers.delete(key) } } abortAll(): void { - for (const [, controller] of this.controllers) { - controller.abort() + const buckets = Array.from(this.byClient.values()) + this.byClient.clear() + for (const requests of buckets) { + for (const controller of requests.values()) { + controller.abort() + } } - this.controllers.clear() - } - - private key(clientId: number, requestId: number): string { - return `${clientId}:${requestId}` } } diff --git a/src/relay/dispatcher-capacity-signals.ts b/src/relay/dispatcher-capacity-signals.ts index 5f945f5c84f..147db516041 100644 --- a/src/relay/dispatcher-capacity-signals.ts +++ b/src/relay/dispatcher-capacity-signals.ts @@ -55,7 +55,7 @@ export abstract class RelayDispatcherCapacitySignals extends RelayDispatcherClie } get legacyRetentionBelowLowWater(): boolean { - return this.publicationLedger.belowLowWater(this.activeClientKeys()) + return this.publicationLedger.belowLowWater(() => this.activeClientKeys()) } /** @@ -138,7 +138,7 @@ export abstract class RelayDispatcherCapacitySignals extends RelayDispatcherClie this.deferredLegacyCapacity ||= !force return } - if (!force && !this.publicationLedger.belowLowWater(this.activeClientKeys())) { + if (!force && !this.publicationLedger.belowLowWater(() => this.activeClientKeys())) { return } for (const listener of this.legacyCapacityListeners) { diff --git a/src/relay/dispatcher-per-connection-state-baseline.test.ts b/src/relay/dispatcher-per-connection-state-baseline.test.ts new file mode 100644 index 00000000000..70d5b325b71 --- /dev/null +++ b/src/relay/dispatcher-per-connection-state-baseline.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it, vi, afterEach } from 'vitest' +import { RelayDispatcher } from './dispatcher' + +// Why a census rather than a duration: "the dispatcher releases per-connection state" is a +// statement about what is still *held* after churn, so it is measured by counting containers, +// not by timing a teardown. Every number here is exact and load-independent. + +type Probed = { + attachClient: (w: (b: Buffer) => void) => number + detachClient: (id: number) => void + onClientCapacity: (id: number, listener: () => void) => (() => void) | null + clients: Map + requestHandlers: Map + notificationHandlers: Map + requestAborts: { + byClient: Map> + create: (clientId: number, requestId: number) => unknown + } + publicationLedger: { clientBytes: Map; aggregateBytes: number } + pendingRelayRequests: Map + clientDetachListeners: Set + disposeListeners: Set + legacyCapacityListeners: Set + clientCapacityListeners: Map + ptyDataPublicationAdmission: unknown + keepaliveTimer: unknown + activeClients: () => unknown[] + tryPublishToClients: (clients: unknown[], msg: unknown, lane: string) => boolean + dispose: () => void +} + +function countAbortControllers(d: Probed): number { + let total = 0 + for (const bucket of d.requestAborts.byClient.values()) { + total += bucket.size + } + return total +} + +function census(d: Probed): Record { + return { + clients: d.clients.size, + requestHandlers: d.requestHandlers.size, + notificationHandlers: d.notificationHandlers.size, + requestAbortControllers: countAbortControllers(d), + ledgerClientBytes: d.publicationLedger.clientBytes.size, + ledgerAggregateBytes: d.publicationLedger.aggregateBytes, + pendingRelayRequests: d.pendingRelayRequests.size, + clientDetachListeners: d.clientDetachListeners.size, + disposeListeners: d.disposeListeners.size, + legacyCapacityListeners: d.legacyCapacityListeners.size, + clientCapacityListeners: d.clientCapacityListeners.size, + ptyDataPublicationAdmission: d.ptyDataPublicationAdmission === null ? 'null' : 'set', + keepaliveTimer: d.keepaliveTimer === null ? 'null' : 'armed' + } +} + +function newDispatcher(): Probed { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Probed names the protected containers this census counts. RelayDispatcher really has them; the compiler just will not hand them out. + return new RelayDispatcher(() => {}) as unknown as Probed +} + +const CLIENTS_PER_CYCLE = 100 + +describe('relay dispatcher per-connection state', () => { + afterEach(() => vi.useRealTimers()) + + it('returns every per-connection container to baseline across repeated churn', () => { + vi.useFakeTimers() + const d = newDispatcher() + const baseline = census(d) + + for (let cycle = 0; cycle < 3; cycle++) { + const ids: number[] = [] + for (let i = 0; i < CLIENTS_PER_CYCLE; i++) { + ids.push(d.attachClient(() => {})) + } + for (const id of ids) { + d.onClientCapacity(id, () => {}) + d.requestAborts.create(id, 1) + } + + // The ledger is the one container with no per-client teardown: an entry is reclaimed by its + // own lease's release(), never by closeClient. `ledgerClientBytes` returning to baseline + // below is therefore load-bearing -- it is the proof that normal closes settle every queued + // and in-flight entry. An entry that did somehow survive a close would not be reclaimed, and + // that is a gap to close, not a contract to pin. + // + // The census must be able to find things: these two are the containers that stay 0 unless + // deliberately loaded, so assert they actually moved before trusting that they came back. + expect(census(d).clients).toBe(CLIENTS_PER_CYCLE + 1) + expect(census(d).clientCapacityListeners).toBe(CLIENTS_PER_CYCLE) + expect(census(d).requestAbortControllers).toBe(CLIENTS_PER_CYCLE) + + d.tryPublishToClients( + d.activeClients(), + { jsonrpc: '2.0', method: 'pty.data', params: { d: 'x'.repeat(256) } }, + 'bulk' + ) + for (const id of ids) { + d.detachClient(id) + } + expect(census(d)).toEqual(baseline) + } + d.dispose() + }) +}) diff --git a/src/relay/dispatcher-rpc-cancel-id-coercion.test.ts b/src/relay/dispatcher-rpc-cancel-id-coercion.test.ts new file mode 100644 index 00000000000..9a015ea10c5 --- /dev/null +++ b/src/relay/dispatcher-rpc-cancel-id-coercion.test.ts @@ -0,0 +1,43 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { RelayDispatcher } from './dispatcher' +import { encodeFrame, MessageType } from './protocol' + +// Why both id shapes: parseJsonRpcMessage only checks the version, so a request id may arrive as a +// string, while rpc.cancel coerces its id through Number(...). The abort index must file both +// under one key or the cancel for a string-id request is silently dropped. +describe('rpc.cancel request-id coercion', () => { + let dispatcher: RelayDispatcher + + beforeEach(() => { + vi.useFakeTimers() + dispatcher = new RelayDispatcher(() => {}) + }) + + afterEach(() => { + dispatcher.dispose() + vi.useRealTimers() + }) + + it.each([ + { label: 'numeric', requestId: 7, cancelId: 7 }, + { label: 'string', requestId: '7', cancelId: '7' }, + { label: 'string request, numeric cancel', requestId: '7', cancelId: 7 } + ])('aborts an in-flight request with a $label id', async ({ requestId, cancelId }) => { + let signal: AbortSignal | undefined + dispatcher.onRequest('test.slow', (_params, ctx) => { + signal = ctx.signal + return new Promise(() => {}) + }) + // Raw frames: the typed encoder would not admit a string id, and that is the point. + const rawFrame = (msg: Record, seq: number): Buffer => + encodeFrame(MessageType.Regular, seq, 0, Buffer.from(JSON.stringify(msg), 'utf-8')) + + dispatcher.feed(rawFrame({ jsonrpc: '2.0', id: requestId, method: 'test.slow' }, 1)) + await vi.advanceTimersByTimeAsync(0) + expect(signal?.aborted).toBe(false) + + dispatcher.feed(rawFrame({ jsonrpc: '2.0', method: 'rpc.cancel', params: { id: cancelId } }, 2)) + + expect(signal?.aborted).toBe(true) + }) +}) diff --git a/src/relay/legacy-relay-publication-ledger.ts b/src/relay/legacy-relay-publication-ledger.ts index f7002756bc2..c641c9a0bd5 100644 --- a/src/relay/legacy-relay-publication-ledger.ts +++ b/src/relay/legacy-relay-publication-ledger.ts @@ -83,11 +83,18 @@ export class LegacyRelayPublicationLedger { }) } - belowLowWater(clientKeys?: readonly string[]): boolean { + // Why the thunk overload: the aggregate ceiling below decides on its own most of the time, and it + // decides *first*. A caller passing an eager array has already built one string per client before + // learning the keys were never going to be read -- and it pays that most in the loaded case, + // because that is exactly when the aggregate check short-circuits. + belowLowWater(clientKeys?: readonly string[] | (() => readonly string[])): boolean { if (this.aggregateBytes > this.relayLowBytes) { return false } - const keys = clientKeys ?? Array.from(this.clientBytes.keys()) + const keys = + typeof clientKeys === 'function' + ? clientKeys() + : (clientKeys ?? Array.from(this.clientBytes.keys())) return keys.every((clientKey) => (this.clientBytes.get(clientKey) ?? 0) <= this.clientLowBytes) } diff --git a/src/relay/relay-hot-path-operation-counts.test.ts b/src/relay/relay-hot-path-operation-counts.test.ts new file mode 100644 index 00000000000..c864f058cc5 --- /dev/null +++ b/src/relay/relay-hot-path-operation-counts.test.ts @@ -0,0 +1,205 @@ +import { describe, expect, it, vi, afterEach } from 'vitest' +import { RelayDispatcher } from './dispatcher' +import { ClientRequestAborts } from './client-request-aborts' + +// Why operation counts and not milliseconds: each assertion below is about how many entries a hot +// path visits, which is the property. A duration is only a proxy for it, and a proxy needs a +// threshold calibrated against observed runtimes -- which makes the test about the observation. +// These counts are exact and identical under any machine load. + +/** Counts entries yielded by a real Map's iterators without changing the code under test. */ +class CountingMap extends Map { + visits = 0 + getCalls = 0 + + private countingIterator(inner: MapIterator): MapIterator { + const bump = (): void => { + this.visits++ + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the literal returned here implements next() and [Symbol.iterator](), which is the whole protocol a for..of over this wrapper reaches; no other IterableIterator member is ever called. + return { + next(): IteratorResult { + const r = inner.next() + if (!r.done) { + bump() + } + return r + }, + [Symbol.iterator]() { + return this + } + } as MapIterator + } + + override [Symbol.iterator](): MapIterator<[K, V]> { + return this.countingIterator(super[Symbol.iterator]()) + } + + override values(): MapIterator { + return this.countingIterator(super.values()) + } + + override get(key: K): V | undefined { + this.getCalls++ + return super.get(key) + } +} + +type ProbedDispatcher = { + attachClient: (w: (b: Buffer) => void) => number + clients: Map + publicationLedger: { + clientBytes: Map + readonly retainedBytes: number + readonly relayLowBytes: number + readonly clientHighBytes: number + tryReserve: (m: readonly { clientKey: string; bytes: number }[]) => unknown[] | null + } + notifyLegacyCapacityIfLow: () => void + activeClients: () => unknown[] + activeClientKeys: () => string[] + tryPublishToClients: (clients: unknown[], msg: unknown, lane: string) => boolean + dispose: () => void +} + +/** Reserves through the real lease path until aggregate retention clears the relay low-water mark. */ +function loadLedgerAboveLowWater(d: ProbedDispatcher): void { + const ledger = d.publicationLedger + for (const clientKey of d.activeClientKeys()) { + if (ledger.retainedBytes > ledger.relayLowBytes) { + return + } + ledger.tryReserve([{ clientKey, bytes: ledger.clientHighBytes }]) + } +} + +function dispatcherWithClients(clientCount: number): { + d: ProbedDispatcher + clients: CountingMap + ledger: CountingMap +} { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ProbedDispatcher names the protected members this census reads. RelayDispatcher really has them; the compiler just will not hand them out. + const d = new RelayDispatcher(() => {}) as unknown as ProbedDispatcher + for (let i = 1; i < clientCount; i++) { + d.attachClient(() => {}) + } + const clients = new CountingMap() + for (const [k, v] of d.clients) { + clients.set(k, v) + } + d.clients = clients + const ledger = new CountingMap() + d.publicationLedger.clientBytes = ledger + clients.visits = 0 + ledger.getCalls = 0 + return { d, clients, ledger } +} + +describe('relay hot-path operation counts', () => { + afterEach(() => vi.useRealTimers()) + + // Why this is the guard and not a duration: abortClient runs on every closeClient and every + // setWrite. Under the flat composite-key map it replaced, one client's teardown enumerated every + // controller in the relay, so a full churn of N clients holding K requests cost K*N*(N+1)/2 visits + // -- measured at 50 -> 5,100, 100 -> 20,200, 200 -> 80,400, 400 -> 320,800, exactly 4x per + // doubling. Teardown must now visit only what the client owns, and must not enumerate the client + // index at all: enumerating it *is* the old scan. + it('abortClient visits only the target client, and never enumerates the client index', () => { + const clientCount = 40 + const inFlightPerClient = 4 + const aborts = new ClientRequestAborts() + for (let c = 1; c <= clientCount; c++) { + for (let r = 1; r <= inFlightPerClient; r++) { + aborts.create(c, r) + } + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: byClient is the private index this test exists to measure; the shape mirrors its declaration in client-request-aborts.ts. + const byClient = (aborts as unknown as { byClient: Map> }) + .byClient + + // The census must be able to find things: prove the maps really hold 160 controllers across 40 + // buckets before asserting that a teardown only touches 4 of them. + expect(byClient.size).toBe(clientCount) + let totalControllers = 0 + for (const bucket of byClient.values()) { + totalControllers += bucket.size + } + expect(totalControllers).toBe(clientCount * inFlightPerClient) + + const index = new CountingMap>() + for (const [k, v] of byClient) { + index.set(k, v) + } + const targetBucket = new CountingMap() + for (const [k, v] of byClient.get(1)!) { + targetBucket.set(k, v) + } + index.set(1, targetBucket) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: swaps the counting stand-in into the same private index read above. + ;(aborts as unknown as { byClient: Map }).byClient = index + index.visits = 0 + targetBucket.visits = 0 + + aborts.abortClient(1) + + expect(targetBucket.visits).toBe(inFlightPerClient) + expect(index.visits).toBe(0) + expect(index.has(1)).toBe(false) + }) + + // Scope: this is the idle arm, where every key has to be read whatever the call shape is. It + // guards against a per-client lookup becoming a per-client scan; it does NOT guard the thunk -- + // the counts below are identical with and without it. The loaded arm is the next test. + it('notifyLegacyCapacity costs exactly one ledger lookup per active client when idle', () => { + vi.useFakeTimers() + for (const clientCount of [50, 100, 200, 400]) { + const { d, clients, ledger } = dispatcherWithClients(clientCount) + + d.notifyLegacyCapacityIfLow() + + expect(clients.visits, `clients enumerated at n=${clientCount}`).toBe(clientCount) + expect(ledger.getCalls, `ledger lookups at n=${clientCount}`).toBe(clientCount) + d.dispose() + } + }) + + // Why the loaded ledger is the one that measures the thunk: the aggregate ceiling answers first + // and on its own, so a caller passing an eager array has already built one key string per client + // before learning they were never going to be read. That is the whole saving, and it is invisible + // below the low-water mark -- which is why counting an idle dispatcher guards nothing. + it('does not enumerate clients at all once the aggregate ceiling answers', () => { + vi.useFakeTimers() + for (const clientCount of [50, 100, 200, 400]) { + const { d, clients, ledger } = dispatcherWithClients(clientCount) + loadLedgerAboveLowWater(d) + // The census must be able to find things: a reserve that silently failed would leave the + // ledger idle and make every count below pass for the wrong reason. + expect(d.publicationLedger.retainedBytes).toBeGreaterThan(d.publicationLedger.relayLowBytes) + clients.visits = 0 + ledger.getCalls = 0 + + d.notifyLegacyCapacityIfLow() + + expect(clients.visits, `clients enumerated at n=${clientCount}`).toBe(0) + expect(ledger.getCalls, `ledger lookups at n=${clientCount}`).toBe(0) + d.dispose() + } + }) + + it('one broadcast publication costs a fixed number of lookups per subscriber', () => { + vi.useFakeTimers() + for (const clientCount of [10, 20, 40]) { + const { d, clients, ledger } = dispatcherWithClients(clientCount) + + d.tryPublishToClients( + d.activeClients(), + { jsonrpc: '2.0', method: 'pty.data', params: { d: 'x' } }, + 'bulk' + ) + + expect(clients.visits, `clients enumerated at n=${clientCount}`).toBe(clientCount * 2) + expect(ledger.getCalls, `ledger lookups at n=${clientCount}`).toBe(clientCount * 4) + d.dispose() + } + }) +}) From 9641a1b5440e387c12108de7296f2d57d2ab0a6b Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 18 Sep 2026 01:04:27 -0400 Subject: [PATCH 52/59] feat(mobile-web-bundle): serve the packaged mobile web bundle over RPC (OTA phase A, 3/5) (#21348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mobile-web-bundle): serve the bundle manifest and chunks over RPC Two paired-runtime methods on the already-authenticated connection: `mobileWeb.bundle.manifest` returns this install's manifest plus the chunk size it advertises, and `mobileWeb.bundle.chunk` returns one aligned range of one asset with the whole asset's length and hash, so a single chunk describes what it belongs to. `path` is accepted only by exact match against a manifest member, so traversal is unreachable rather than mitigated. Each asset's on-disk sha256 is verified once and the verdict remembered, concurrent first readers sharing one hash. Reads are capped at four in flight per connection, and a disconnected client stops costing reads at the next checkpoint. No SSH or relay proxying: a runtime answers only out of its own install. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile-web-bundle): pin the three buildId serializers against each other The canonical serialization exists in the builder, the packaging guard, and the shared contract, because the two packaging scripts run on bare node before any build output exists and cannot import TypeScript. A divergence in any one would reject every honest bundle at packaging, or ship a bundle whose id the phone recomputes differently and re-downloads forever. Proved red by swapping the guard's code-unit sort for localeCompare: five of six cases fail. Exports the guard's serializer for the test; no packaging behaviour changes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile-web-bundle): cover every error code and a multi-chunk paging round trip Against a synthetic bundle in a temp dir, because the real builder's largest asset is under one chunk and CI unit jobs never build out/mobile-web. The fixture's script spans three chunks, its stylesheet is exactly one, and one asset is empty, so paging, the eof boundary, and the zero-byte case are exercised rather than assumed. Reads in flight are held by latching `open`, so the four-per-connection cap and an abort arriving mid-read are deterministic rather than a race with a stopwatch. Both were proved red: dropping the abort check after verification fails the abort case, and keying the cap on connectionId alone fails the device-token case. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web-bundle): resolve the bundle root through the AppEnvironment port check:runtime-electron-ratchet caught this: the resolver sat beside getBundledWebClientRoot in src/main/startup and imported electron, and importing it from an RPC method pulled the first electron edge into a runtime graph whose baseline is zero. The runtime has to stay bootable on plain Node. So it reads app.getAppPath() through the port every other runtime module already uses, and moves next to its two callers under src/main/runtime. A host with no environment installed has no install root, which is the same answer as having no bundle. orcad answers getAppPath from its own install root, so a headless runtime that carries the artifact serves it with no special case. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile-web-bundle): cover the resolver's two probe layouts directly Also stops exporting the manifest filename, which nothing outside the resolver needs. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile-web-bundle): pin both methods on the mobile allowlist The scanner only checks mobile-used ⊆ allowlist, and no mobile source calls these until A5, so deleting both entries left every test green. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web-bundle): keep filesystem failures inside the six error codes An asset unlinked or truncated after its verdict was cached reached the client as runtime_error carrying the desktop's absolute install path. Both now answer mobile_web_bundle_asset_changed, with the cause warned host-side only. A short positional read is the truncation case, so it throws instead of paging the client past the end. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-web-bundle): drop the unreachable release-idempotence guard The one caller releases exactly once in a finally; removing the flag left every test green, so it was defensiveness against a caller that does not exist. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile-web-bundle): prove a failed verify is not cached as a verdict The verdict cache never invalidates, so a transient read failure remembered as a verdict would poison the asset for the life of the process. Removing the delete left every test green until now. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-web-bundle): delete the unsatisfiable manifest params schema The dispatcher substitutes `{}` for absent params, so `z.null()` could never parse; the method declares `params: null` instead. A comment on the method name records why there is no schema. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web-bundle): fill the read window instead of failing a partial read fs.read may answer short of what it was asked for before EOF, so the previous check turned a legitimate partial read into a spurious asset_changed. The loop mirrors the relay's readFullStreamChunk, which is not imported because it sits behind the relay dispatcher's module graph; only a read returning nothing is treated as truncation. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-web-bundle): read the disconnect idiom with the shared predicate isClientDisconnectedError already exports exactly the check the catch needed, so the local error class goes away and the throw returns to the repo-wide idiom. The module doc now says asContractError is a total catch. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile-web-bundle): pin the four branches no test was holding Each one survived a mutation: the abort check before verification, the per-process manifest cache, the buildId component of the verdict key, and delete-at-zero in the admission map. The last two matter beyond hygiene — a verdict keyed by path alone carries a failed verdict onto the next build of index.html, and a map that never drops a key retains one pairing token per socket. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- ...bile-web-bundle-serializer-parity.test.mjs | 119 ++++ .../verify-packaged-mobile-web-bundle.cjs | 4 +- src/main/runtime/bundled-mobile-web-bundle.ts | 85 +++ src/main/runtime/rpc/methods/index.ts | 2 + .../methods/mobile-web-bundle-asset-reader.ts | 122 ++++ .../mobile-web-bundle-read-admission.ts | 45 ++ ...mobile-web-bundle-read-concurrency.test.ts | 253 ++++++++ .../methods/mobile-web-bundle.test-fixture.ts | 106 ++++ .../rpc/methods/mobile-web-bundle.test.ts | 552 ++++++++++++++++++ .../runtime/rpc/methods/mobile-web-bundle.ts | 145 +++++ .../runtime-rpc-mobile-method-allowlist.ts | 2 + .../bundle-rpc-contract.test.ts | 6 - .../mobile-web-bundle/bundle-rpc-contract.ts | 5 +- .../rpc-params-catalog.generated.ts | 3 + 14 files changed, 1439 insertions(+), 10 deletions(-) create mode 100644 config/scripts/mobile-web-bundle-serializer-parity.test.mjs create mode 100644 src/main/runtime/bundled-mobile-web-bundle.ts create mode 100644 src/main/runtime/rpc/methods/mobile-web-bundle-asset-reader.ts create mode 100644 src/main/runtime/rpc/methods/mobile-web-bundle-read-admission.ts create mode 100644 src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts create mode 100644 src/main/runtime/rpc/methods/mobile-web-bundle.test-fixture.ts create mode 100644 src/main/runtime/rpc/methods/mobile-web-bundle.test.ts create mode 100644 src/main/runtime/rpc/methods/mobile-web-bundle.ts diff --git a/config/scripts/mobile-web-bundle-serializer-parity.test.mjs b/config/scripts/mobile-web-bundle-serializer-parity.test.mjs new file mode 100644 index 00000000000..9b82a09265b --- /dev/null +++ b/config/scripts/mobile-web-bundle-serializer-parity.test.mjs @@ -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) + }) +}) diff --git a/config/scripts/verify-packaged-mobile-web-bundle.cjs b/config/scripts/verify-packaged-mobile-web-bundle.cjs index 13cf71b008b..c521882691d 100644 --- a/config/scripts/verify-packaged-mobile-web-bundle.cjs +++ b/config/scripts/verify-packaged-mobile-web-bundle.cjs @@ -188,4 +188,6 @@ function assertMobileWebBundleBuilt(bundleDir = MOBILE_WEB_BUNDLE_DIR) { return manifest } -module.exports = { MOBILE_WEB_BUNDLE_DIR, assertMobileWebBundleBuilt } +// 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 } diff --git a/src/main/runtime/bundled-mobile-web-bundle.ts b/src/main/runtime/bundled-mobile-web-bundle.ts new file mode 100644 index 00000000000..b3ffb06f912 --- /dev/null +++ b/src/main/runtime/bundled-mobile-web-bundle.ts @@ -0,0 +1,85 @@ +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { getAppEnvironment, hasAppEnvironment } from '../../shared/app-environment' +import { + MobileWebBundleManifestSchema, + type MobileWebBundleManifest +} from '../../shared/mobile-web-bundle/manifest-contract' + +const MANIFEST_FILENAME = 'manifest.json' + +export type BundledMobileWebBundle = { + root: string + manifest: MobileWebBundleManifest +} + +/** + * Probed exactly like getBundledWebClientRoot: the bundle ships inside app.asar under out/, so the + * two entrypoint layouts that move appPath are the only ones that can move it. + * + * Read through the AppEnvironment port rather than `electron.app`, because this module is reachable + * from the runtime's import graph and the runtime must stay bootable on plain Node. A host with no + * environment installed has no install root, which is the same answer as having no bundle. + */ +export function getBundledMobileWebBundleRoot(): string | undefined { + if (!hasAppEnvironment()) { + return undefined + } + const appPath = getAppEnvironment().getAppPath() + const roots = [ + join(appPath, 'out', 'mobile-web'), + // Why: unpacked electron-vite entrypoints set appPath to out/main, next to the bundle. + join(appPath, '..', 'mobile-web') + ] + return roots.find((root) => existsSync(join(root, MANIFEST_FILENAME))) +} + +// Why no invalidation: the bundle is immutable for the life of the install, and an auto-update +// replaces it only by restarting the app, so a stale entry cannot outlive the process that read it. +// `undefined` means "not looked at yet", `null` means "looked, and this install has no bundle". +let cachedBundle: BundledMobileWebBundle | null | undefined + +export function loadBundledMobileWebBundle(): BundledMobileWebBundle | null { + if (cachedBundle === undefined) { + cachedBundle = readBundledMobileWebBundle() + } + return cachedBundle +} + +/** Tests own the process, so they own the cache; nothing in the app may call this. */ +export function resetBundledMobileWebBundleCacheForTests(): void { + cachedBundle = undefined +} + +function readBundledMobileWebBundle(): BundledMobileWebBundle | null { + const root = getBundledMobileWebBundleRoot() + if (!root) { + return null + } + const manifestPath = join(root, MANIFEST_FILENAME) + let raw: string + try { + raw = readFileSync(manifestPath, 'utf8') + } catch (error) { + console.warn(`[mobile-web-bundle] cannot read ${manifestPath}:`, error) + return null + } + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch (error) { + console.warn(`[mobile-web-bundle] ${manifestPath} is not valid JSON:`, error) + return null + } + const manifest = MobileWebBundleManifestSchema.safeParse(parsed) + if (!manifest.success) { + // Why warn rather than throw: packaging already hash-verifies the bundle, so reaching here means + // a dev or hand-edited out/, and an unusable bundle must degrade to "no bundle", never to a + // crash on a path a phone can reach. + console.warn(`[mobile-web-bundle] ${manifestPath} does not match the manifest contract:`, { + issues: manifest.error.issues + }) + return null + } + return { root, manifest: manifest.data } +} diff --git a/src/main/runtime/rpc/methods/index.ts b/src/main/runtime/rpc/methods/index.ts index a247c8bbe41..cbf6e0d8e0c 100644 --- a/src/main/runtime/rpc/methods/index.ts +++ b/src/main/runtime/rpc/methods/index.ts @@ -38,6 +38,7 @@ import { PLUGIN_METHODS } from './plugins' import { SKILL_METHODS } from './skills' import { CLIPBOARD_METHODS } from './clipboard' import { HOST_CAPABILITY_METHODS } from './host-capabilities' +import { MOBILE_WEB_BUNDLE_METHODS } from './mobile-web-bundle' import { RUNTIME_CLIENT_CAPABILITY_METHODS } from './runtime-client-capabilities' import { EMULATOR_METHODS } from './emulator' import { PAIRING_METHODS } from './pairing' @@ -95,6 +96,7 @@ export const ALL_RPC_METHODS = [ ...SKILL_METHODS, ...CLIPBOARD_METHODS, ...HOST_CAPABILITY_METHODS, + ...MOBILE_WEB_BUNDLE_METHODS, ...RUNTIME_CLIENT_CAPABILITY_METHODS, ...CLIENT_EVENT_METHODS, ...CLIENT_UI_METHODS, diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle-asset-reader.ts b/src/main/runtime/rpc/methods/mobile-web-bundle-asset-reader.ts new file mode 100644 index 00000000000..1cc87e02389 --- /dev/null +++ b/src/main/runtime/rpc/methods/mobile-web-bundle-asset-reader.ts @@ -0,0 +1,122 @@ +import { createHash } from 'node:crypto' +import { open } from 'node:fs/promises' +import { join } from 'node:path' +import type { MobileWebBundleAsset } from '../../../../shared/mobile-web-bundle/manifest-contract' + +// Why keyed by buildId as well as path: buildId is a content hash, so a dev rebuild that swaps the +// bundle under a running app can never reuse a verdict recorded against the previous bytes. +const verdicts = new Map>() + +/** Tests own the process, so they own the cache; nothing in the app may call this. */ +export function resetMobileWebBundleAssetVerdictsForTests(): void { + verdicts.clear() +} + +/** + * Whether the bytes on disk still hash to what the manifest promised, computed once per asset and + * then remembered. Concurrent first readers share one hash: the promise goes into the map before + * the first await, so four parallel chunk requests for the same asset read it once, not four times. + */ +export function verifyMobileWebBundleAsset( + root: string, + buildId: string, + asset: MobileWebBundleAsset +): Promise { + const key = `${buildId} ${asset.path}` + const cached = verdicts.get(key) + if (cached) { + return cached + } + const verdict = hashAsset(root, asset).then(undefined, (error: unknown) => { + // A read that failed is not evidence the bytes changed, so it is not remembered as a verdict. + verdicts.delete(key) + throw error + }) + verdicts.set(key, verdict) + return verdict +} + +async function hashAsset(root: string, asset: MobileWebBundleAsset): Promise { + const handle = await open(join(root, asset.path), 'r') + try { + const hash = createHash('sha256') + // Streamed rather than read whole: the contract ceiling is 10 MiB per asset, and this runs on + // the main process's event loop. + for await (const block of handle.createReadStream({ autoClose: false })) { + hash.update(block) + } + return hash.digest('hex') === asset.sha256 + } finally { + await handle.close() + } +} + +/** + * The bytes of one asset in the range starting at `offset`, clamped to the asset's manifest length. + * The window is always filled: a read that stops early only means the file really ended, which the + * caller answers as a changed asset instead of paging a client past a truncation. + * + * Measured through asar (Electron 43): `open` hands back a descriptor on a per-asset copy the asar + * layer materialises once under the OS temp dir and then reuses for the life of the process, so a + * positional read costs one pread and never re-inflates the archive. Nothing to cache here. + */ +export async function readMobileWebBundleAssetChunk( + root: string, + asset: MobileWebBundleAsset, + offset: number, + length: number +): Promise { + const wanted = Math.min(length, Math.max(0, asset.byteLength - offset)) + const buffer = Buffer.alloc(wanted) + if (wanted === 0) { + return buffer + } + // `asset.path` is a manifest member the caller matched exactly, never a client string, and the + // manifest schema already rejects absolute paths, backslashes, and traversal segments. + const handle = await open(join(root, asset.path), 'r') + try { + const filled = await fillMobileWebBundleReadWindow(handle, buffer, wanted, offset) + if (filled !== wanted) { + throw new Error( + `short read of ${asset.path}: ${String(filled)} of ${String(wanted)} bytes at ${String(offset)}` + ) + } + return buffer + } finally { + await handle.close() + } +} + +/** Just the member the window fill needs, so it can be driven by a stub, like the relay's + * `readFullStreamChunk` it mirrors. That one is not imported: it sits behind the relay + * dispatcher's module graph, which the runtime bundle has no business pulling in. */ +type PositionalReader = { + read( + buffer: Buffer, + offset: number, + length: number, + position: number + ): Promise<{ bytesRead: number }> +} + +/** + * Bytes actually placed in `buffer`, reading until the window is full. `read` may answer short of + * what it was asked for before EOF, so a single call is not evidence of anything; only a read that + * returns nothing means the file ended early. + */ +export async function fillMobileWebBundleReadWindow( + reader: PositionalReader, + buffer: Buffer, + wanted: number, + offset: number +): Promise { + let filled = 0 + while (filled < wanted) { + const { bytesRead } = await reader.read(buffer, filled, wanted - filled, offset + filled) + if (bytesRead === 0) { + break + } + filled += bytesRead + } + return filled +} diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle-read-admission.ts b/src/main/runtime/rpc/methods/mobile-web-bundle-read-admission.ts new file mode 100644 index 00000000000..505ca2b9391 --- /dev/null +++ b/src/main/runtime/rpc/methods/mobile-web-bundle-read-admission.ts @@ -0,0 +1,45 @@ +import type { RpcContext } from '../core' + +/** Enough for a client to keep the pipe full without letting one phone own the disk. */ +export const MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS = 4 + +const activeReads = new Map() + +/** Tests own the process, so they own the counters; nothing in the app may call this. */ +export function resetMobileWebBundleReadAdmissionForTests(): void { + activeReads.clear() +} + +/** Buckets currently holding at least one read. Exported so a test can prove the map does not + * retain a device token per socket; nothing in the app may call this. */ +export function mobileWebBundleReadBucketCountForTests(): number { + return activeReads.size +} + +/** + * The bucket a chunk read is charged to. `connectionId` is set only for E2EE mobile sockets, so + * keying on it alone would leave a plain-WebSocket phone in one shared unbounded bucket; the device + * token still names one client. An in-process caller has neither and is not the caller this bounds. + */ +export function mobileWebBundleReadBucket(ctx: RpcContext): string { + return ctx.connectionId ?? ctx.clientId ?? 'local' +} + +/** A slot in the bucket's budget, or null when it is already full. Release exactly once. */ +export function acquireMobileWebBundleReadSlot(bucket: string): (() => void) | null { + const active = activeReads.get(bucket) ?? 0 + if (active >= MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS) { + return null + } + activeReads.set(bucket, active + 1) + return () => { + const remaining = (activeReads.get(bucket) ?? 1) - 1 + // Dropping the key at zero is what keeps this from retaining one entry per socket forever — + // and off the E2EE channel the key is the device's pairing token. + if (remaining > 0) { + activeReads.set(bucket, remaining) + } else { + activeReads.delete(bucket) + } + } +} diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts b/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts new file mode 100644 index 00000000000..9d628182083 --- /dev/null +++ b/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts @@ -0,0 +1,253 @@ +/** + * The behaviours that only exist while a read is genuinely in flight or genuinely failing: the + * per-connection cap, an abort that arrives mid-read, and a verify whose open throws. All three go + * through a gate on `open`, so none of them depends on a race between an event loop and a stopwatch. + */ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type * as FsPromises from 'node:fs/promises' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { RpcResponse } from '../core' +import type { RpcDispatcher } from '../dispatcher' + +/** A latch on `open`, so a read can be held mid-flight without racing a stopwatch. */ +type OpenGate = { + blocker: Promise | null + unlatch: (() => void) | null + opens: number + failures: number + hold(): void + release(): void + failNextOpen(): void + reset(): void +} + +const { gate } = vi.hoisted(() => { + const gate: OpenGate = { + blocker: null, + unlatch: null, + opens: 0, + failures: 0, + hold() { + gate.blocker = new Promise((resolve) => { + gate.unlatch = resolve + }) + }, + release() { + gate.unlatch?.() + gate.blocker = null + gate.unlatch = null + }, + failNextOpen() { + gate.failures++ + }, + reset() { + gate.release() + gate.opens = 0 + gate.failures = 0 + } + } + return { gate } +}) + +vi.mock('node:fs/promises', async () => { + const actual = await vi.importActual('node:fs/promises') + return { + ...actual, + default: actual, + open: async (...args: Parameters) => { + gate.opens++ + if (gate.blocker) { + await gate.blocker + } + if (gate.failures > 0) { + gate.failures-- + throw new Error('EIO: i/o error, open') + } + return actual.open(...args) + } + } +}) + +import { resetBundledMobileWebBundleCacheForTests } from '../../bundled-mobile-web-bundle' +import { resetMobileWebBundleAssetVerdictsForTests } from './mobile-web-bundle-asset-reader' +import { + MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS, + resetMobileWebBundleReadAdmissionForTests +} from './mobile-web-bundle-read-admission' +import { + installMobileWebBundleAppPath, + mobileWebBundleDispatcher, + writeSyntheticMobileWebBundle, + type SyntheticMobileWebBundle +} from './mobile-web-bundle.test-fixture' + +let scratch: string +let bundle: SyntheticMobileWebBundle +let dispatcher: RpcDispatcher + +type DispatchOptions = { connectionId?: string; signal?: AbortSignal } + +function chunk(offset: number, options?: DispatchOptions): Promise { + return dispatcher.dispatch( + { + id: `chunk-${String(offset)}`, + authToken: 'tok', + method: 'mobileWeb.bundle.chunk', + params: { buildId: bundle.buildId, path: 'index.html', offset } + }, + options + ) +} + +function errorMessage(response: RpcResponse): string | undefined { + return response.ok ? undefined : response.error.message +} + +/** Lets every already-scheduled continuation run, without advancing any clock. */ +async function settleMicrotasks(): Promise { + for (let turn = 0; turn < 20; turn++) { + await Promise.resolve() + } +} + +beforeEach(() => { + scratch = mkdtempSync(join(tmpdir(), 'orca-mobile-web-reads-')) + installMobileWebBundleAppPath(scratch) + bundle = writeSyntheticMobileWebBundle(join(scratch, 'out', 'mobile-web'), 7) + gate.reset() + resetBundledMobileWebBundleCacheForTests() + resetMobileWebBundleAssetVerdictsForTests() + resetMobileWebBundleReadAdmissionForTests() + vi.spyOn(console, 'warn').mockImplementation(() => {}) + dispatcher = mobileWebBundleDispatcher() +}) + +afterEach(() => { + gate.reset() + rmSync(scratch, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('chunk reads in flight on one connection', () => { + // Pinned as a literal because every other case here is written in terms of the constant, so the + // budget itself would otherwise move silently with it. + it('budgets four', () => { + expect(MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS).toBe(4) + }) + + it('admits four and refuses the fifth, then admits it once one finishes', async () => { + gate.hold() + const inFlight = Array.from({ length: MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS }, () => + chunk(0, { connectionId: 'conn-1' }) + ) + await settleMicrotasks() + + const overflow = await chunk(0, { connectionId: 'conn-1' }) + expect(errorMessage(overflow)).toBe('mobile_web_bundle_read_limited') + + gate.release() + const admitted = await Promise.all(inFlight) + expect(admitted.every((response) => response.ok)).toBe(true) + + const afterwards = await chunk(0, { connectionId: 'conn-1' }) + expect(afterwards.ok).toBe(true) + }) + + it('does not let one connection at its cap cost another connection a read', async () => { + gate.hold() + const saturating = Array.from({ length: MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS }, () => + chunk(0, { connectionId: 'conn-1' }) + ) + await settleMicrotasks() + + const neighbour = chunk(0, { connectionId: 'conn-2' }) + await settleMicrotasks() + gate.release() + + expect((await neighbour).ok).toBe(true) + expect((await Promise.all(saturating)).every((response) => response.ok)).toBe(true) + }) + + it('hashes an asset once even when four first readers arrive together', async () => { + gate.hold() + const together = Array.from({ length: MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS }, () => + chunk(0, { connectionId: 'conn-1' }) + ) + await settleMicrotasks() + + // One verification open for the four of them; the rest are the four chunk reads. + const opensBeforeRelease = gate.opens + gate.release() + await Promise.all(together) + + expect(opensBeforeRelease).toBe(1) + expect(gate.opens).toBe(1 + MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS) + }) +}) + +describe('a client that disconnects while its chunk is being read', () => { + it('stops before the chunk read, and answers nothing it had already produced', async () => { + const controller = new AbortController() + gate.hold() + const pending = chunk(0, { connectionId: 'conn-3', signal: controller.signal }) + await settleMicrotasks() + expect(gate.opens).toBe(1) + + controller.abort() + gate.release() + const response = await pending + + expect(response.ok).toBe(false) + expect(errorMessage(response)).toBe('client_disconnected') + // The verification open happened before the abort; the chunk read never did. + expect(gate.opens).toBe(1) + }) + + // Honouring `signal` exists so a client that is gone stops costing file reads. Verification + // streams the whole asset, up to the contract's 10 MiB ceiling, so the check that matters is the + // one before it: not a single open. + it('does not hash the asset at all when the signal was already aborted', async () => { + const controller = new AbortController() + controller.abort() + + const response = await chunk(0, { connectionId: 'conn-6', signal: controller.signal }) + + expect(errorMessage(response)).toBe('client_disconnected') + expect(gate.opens).toBe(0) + }) + + it('releases the slot it was holding, so the connection is not permanently capped', async () => { + const aborted = Array.from({ length: MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS }, () => { + const controller = new AbortController() + return { + controller, + response: chunk(0, { connectionId: 'conn-4', signal: controller.signal }) + } + }) + gate.hold() + await settleMicrotasks() + for (const { controller } of aborted) { + controller.abort() + } + gate.release() + await Promise.all(aborted.map(({ response }) => response)) + + expect((await chunk(0, { connectionId: 'conn-4' })).ok).toBe(true) + }) +}) + +describe('a verify whose read of the asset fails', () => { + // The verdict cache is never invalidated, so remembering a transient EIO as "these bytes are + // wrong" would poison the asset until the desktop restarts. + it('is not remembered as a verdict, so the next read still verifies', async () => { + gate.failNextOpen() + + const failed = await chunk(0, { connectionId: 'conn-5' }) + const retried = await chunk(0, { connectionId: 'conn-5' }) + + expect(errorMessage(failed)).toBe('mobile_web_bundle_asset_changed') + expect(retried.ok).toBe(true) + }) +}) diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle.test-fixture.ts b/src/main/runtime/rpc/methods/mobile-web-bundle.test-fixture.ts new file mode 100644 index 00000000000..f955bfabd76 --- /dev/null +++ b/src/main/runtime/rpc/methods/mobile-web-bundle.test-fixture.ts @@ -0,0 +1,106 @@ +import { createHash } from 'node:crypto' +import { mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { installFakeAppEnvironment } from '../../../../../config/scripts/vitest-host-ports-setup' +import type { OrcaRuntimeService } from '../../orca-runtime' +import { RpcDispatcher } from '../dispatcher' +import { MOBILE_WEB_BUNDLE_METHODS } from './mobile-web-bundle' +import { MOBILE_WEB_BUNDLE_CHUNK_BYTES } from '../../../../shared/mobile-web-bundle/bundle-rpc-contract' +import { + computeMobileWebBundleId, + type MobileWebBundleAsset +} from '../../../../shared/mobile-web-bundle/manifest-contract' + +export const sha256Hex = (bytes: Buffer): string => createHash('sha256').update(bytes).digest('hex') + +/** Deterministic, varied bytes, so a read at the wrong offset cannot accidentally look right. */ +export function mobileWebBundleFiller(byteLength: number, seed: number): Buffer { + const bytes = Buffer.alloc(byteLength) + for (let index = 0; index < byteLength; index++) { + bytes[index] = (index * 31 + seed * 17) % 256 + } + return bytes +} + +type SyntheticAsset = { path: string; bytes: Buffer; contentType: string } + +/** + * A bundle the real builder cannot produce today: its largest asset spans three chunks, where every + * asset the Phase A bootstrap emits is under one. Multi-chunk paging has to be exercised rather than + * assumed, and CI unit jobs never build out/mobile-web, so the fixture is synthetic on purpose. + */ +function syntheticAssets(seed: number): SyntheticAsset[] { + const script = mobileWebBundleFiller(MOBILE_WEB_BUNDLE_CHUNK_BYTES * 2 + 1024, seed) + const stylesheet = mobileWebBundleFiller(MOBILE_WEB_BUNDLE_CHUNK_BYTES, seed + 1) + const mark = Buffer.alloc(0) + return [ + { + path: 'index.html', + bytes: mobileWebBundleFiller(640, seed + 2), + contentType: 'text/html; charset=utf-8' + }, + { + path: `assets/${sha256Hex(script)}.js`, + bytes: script, + contentType: 'text/javascript; charset=utf-8' + }, + { path: `assets/${sha256Hex(stylesheet)}.css`, bytes: stylesheet, contentType: 'text/css' }, + { path: `assets/${sha256Hex(mark)}.png`, bytes: mark, contentType: 'image/png' } + ] +} + +export type SyntheticMobileWebBundle = { + root: string + buildId: string + assets: MobileWebBundleAsset[] +} + +export function writeSyntheticMobileWebBundle( + root: string, + seed: number +): SyntheticMobileWebBundle { + mkdirSync(join(root, 'assets'), { recursive: true }) + const written = syntheticAssets(seed) + for (const asset of written) { + writeFileSync(join(root, asset.path), asset.bytes) + } + const assets = written + .map((asset) => ({ + path: asset.path, + sha256: sha256Hex(asset.bytes), + byteLength: asset.bytes.byteLength, + contentType: asset.contentType + })) + .sort((left, right) => (left.path < right.path ? -1 : 1)) + const buildId = computeMobileWebBundleId(assets) + writeFileSync( + join(root, 'manifest.json'), + JSON.stringify({ + schemaVersion: 1, + buildId, + desktopVersion: '1.4.200', + minCompatibleRuntimeProtocolVersion: 2, + runtimeProtocolVersion: 2, + entrypoint: 'index.html', + totalBytes: assets.reduce((total, asset) => total + asset.byteLength, 0), + assets + }), + 'utf8' + ) + return { root, buildId, assets } +} + +/** A dispatcher carrying only these methods. Nothing here reaches the runtime service: the bundle is + * read off the install, so the dispatcher's one call into it is the envelope's runtime id. */ +export function mobileWebBundleDispatcher(): RpcDispatcher { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: neither mobileWeb.bundle method takes a runtime argument, so getRuntimeId (read once, to stamp the envelope) is the only member this dispatcher can reach. + const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService + return new RpcDispatcher({ runtime, methods: MOBILE_WEB_BUNDLE_METHODS }) +} + +/** The install root the resolver probes. Installed through the port, not an electron mock: the + * resolver is reachable from the runtime's import graph and so must never import electron. The + * shared setup reinstalls a default environment before every test, so nothing here needs undoing. */ +export function installMobileWebBundleAppPath(appPath: string): void { + installFakeAppEnvironment({ getAppPath: () => appPath, getPath: () => appPath }) +} diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts b/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts new file mode 100644 index 00000000000..b04c7f18129 --- /dev/null +++ b/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts @@ -0,0 +1,552 @@ +import { mkdirSync, mkdtempSync, rmSync, truncateSync, unlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + MOBILE_WEB_BUNDLE_CHUNK_BYTES, + MOBILE_WEB_BUNDLE_CHUNK_METHOD, + MOBILE_WEB_BUNDLE_MANIFEST_METHOD, + MobileWebBundleChunkResultSchema, + MobileWebBundleManifestResultSchema +} from '../../../../shared/mobile-web-bundle/bundle-rpc-contract' +import { MOBILE_RPC_METHOD_ALLOWLIST } from '../../runtime-rpc/runtime-rpc-mobile-method-allowlist' +import type { RpcRequest, RpcResponse } from '../core' +import type { RpcDispatcher } from '../dispatcher' + +import { + getBundledMobileWebBundleRoot, + resetBundledMobileWebBundleCacheForTests +} from '../../bundled-mobile-web-bundle' +import { + fillMobileWebBundleReadWindow, + resetMobileWebBundleAssetVerdictsForTests +} from './mobile-web-bundle-asset-reader' +import { + acquireMobileWebBundleReadSlot, + MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS, + mobileWebBundleReadBucketCountForTests, + resetMobileWebBundleReadAdmissionForTests +} from './mobile-web-bundle-read-admission' +import { + installMobileWebBundleAppPath, + mobileWebBundleDispatcher, + mobileWebBundleFiller, + sha256Hex, + writeSyntheticMobileWebBundle, + type SyntheticMobileWebBundle +} from './mobile-web-bundle.test-fixture' + +let scratch: string +let dispatcher: RpcDispatcher + +function request(method: string, params?: unknown): RpcRequest { + return { id: `req-${method}`, authToken: 'tok', method, params } +} + +type DispatchOptions = { connectionId?: string; clientId?: string; signal?: AbortSignal } + +async function call(method: string, params?: unknown, options?: DispatchOptions) { + return dispatcher.dispatch(request(method, params), options) +} + +function errorMessage(response: RpcResponse): string | undefined { + return response.ok ? undefined : response.error.message +} + +async function chunk(params: unknown, options?: DispatchOptions) { + return call('mobileWeb.bundle.chunk', params, options) +} + +/** Pages one asset to the end the way a client must: never assuming a size it did not read. */ +async function download(buildId: string, path: string): Promise<{ bytes: Buffer; calls: number }> { + const pieces: Buffer[] = [] + let offset = 0 + let calls = 0 + for (;;) { + const response = await chunk({ buildId, path, offset }) + calls++ + if (!response.ok) { + throw new Error(`chunk at ${String(offset)} failed: ${response.error.message}`) + } + const body = MobileWebBundleChunkResultSchema.parse(response.result) + expect(body.buildId).toBe(buildId) + expect(body.path).toBe(path) + expect(body.offset).toBe(offset) + pieces.push(Buffer.from(body.dataBase64, 'base64')) + if (body.eof) { + expect(offset + pieces.at(-1)!.byteLength).toBe(body.assetByteLength) + break + } + offset += MOBILE_WEB_BUNDLE_CHUNK_BYTES + } + return { bytes: Buffer.concat(pieces), calls } +} + +beforeEach(() => { + scratch = mkdtempSync(join(tmpdir(), 'orca-mobile-web-bundle-')) + installMobileWebBundleAppPath(scratch) + resetBundledMobileWebBundleCacheForTests() + resetMobileWebBundleAssetVerdictsForTests() + resetMobileWebBundleReadAdmissionForTests() + vi.spyOn(console, 'warn').mockImplementation(() => {}) + dispatcher = mobileWebBundleDispatcher() +}) + +afterEach(() => { + rmSync(scratch, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('an install that carries a mobile web bundle', () => { + let bundle: SyntheticMobileWebBundle + + beforeEach(() => { + bundle = writeSyntheticMobileWebBundle(join(scratch, 'out', 'mobile-web'), 1) + }) + + it('answers the manifest with the chunk size it will actually serve', async () => { + const response = await call('mobileWeb.bundle.manifest') + + expect(response.ok).toBe(true) + const body = MobileWebBundleManifestResultSchema.parse( + response.ok ? response.result : undefined + ) + expect(body.chunkBytes).toBe(MOBILE_WEB_BUNDLE_CHUNK_BYTES) + expect(body.manifest.buildId).toBe(bundle.buildId) + expect(body.manifest.assets).toEqual(bundle.assets) + }) + + // Read once per process: without the cache every chunk request re-parses the manifest, and the + // schema's refinement recomputes the buildId with a pure-JS sha256 on the event loop. + it('answers from the manifest it already read, without going back to disk', async () => { + const first = await call('mobileWeb.bundle.manifest') + writeFileSync(join(bundle.root, 'manifest.json'), 'not json', 'utf8') + + const second = await call('mobileWeb.bundle.manifest') + + expect(errorMessage(second)).toBeUndefined() + expect(MobileWebBundleManifestResultSchema.parse(second.ok && second.result).manifest).toEqual( + MobileWebBundleManifestResultSchema.parse(first.ok && first.result).manifest + ) + }) + + it('pages every asset back byte for byte, and each reassembly matches its manifest hash', async () => { + for (const asset of bundle.assets) { + const { bytes, calls } = await download(bundle.buildId, asset.path) + + expect(bytes.byteLength).toBe(asset.byteLength) + expect(sha256Hex(bytes)).toBe(asset.sha256) + expect(calls).toBe(Math.max(1, Math.ceil(asset.byteLength / MOBILE_WEB_BUNDLE_CHUNK_BYTES))) + } + }) + + it('reports eof only on the last chunk of a multi-chunk asset', async () => { + const script = bundle.assets.find((asset) => asset.path.endsWith('.js'))! + expect(script.byteLength).toBeGreaterThan(MOBILE_WEB_BUNDLE_CHUNK_BYTES * 2) + + const eofs: boolean[] = [] + for (let offset = 0; offset < script.byteLength; offset += MOBILE_WEB_BUNDLE_CHUNK_BYTES) { + const response = await chunk({ buildId: bundle.buildId, path: script.path, offset }) + expect(response.ok).toBe(true) + eofs.push(MobileWebBundleChunkResultSchema.parse(response.ok && response.result).eof) + } + + expect(eofs).toEqual([false, false, true]) + }) + + // An asset whose length is an exact multiple of the chunk size must still end somewhere, and the + // only offset a client could try next is one the host rejects. + it('ends an exactly-one-chunk asset on its first chunk', async () => { + const stylesheet = bundle.assets.find((asset) => asset.path.endsWith('.css'))! + expect(stylesheet.byteLength).toBe(MOBILE_WEB_BUNDLE_CHUNK_BYTES) + + const first = await chunk({ buildId: bundle.buildId, path: stylesheet.path, offset: 0 }) + const past = await chunk({ + buildId: bundle.buildId, + path: stylesheet.path, + offset: MOBILE_WEB_BUNDLE_CHUNK_BYTES + }) + + expect(MobileWebBundleChunkResultSchema.parse(first.ok && first.result).eof).toBe(true) + expect(errorMessage(past)).toBe('mobile_web_bundle_offset_invalid') + }) + + // Offset 0 is in range for every asset, including an empty one, so a client never has to special + // case a zero-byte member it cannot ask about. + it('serves a zero-byte asset as one empty chunk at eof', async () => { + const mark = bundle.assets.find((asset) => asset.byteLength === 0)! + + const response = await chunk({ buildId: bundle.buildId, path: mark.path, offset: 0 }) + + const body = MobileWebBundleChunkResultSchema.parse(response.ok && response.result) + expect(body).toMatchObject({ dataBase64: '', eof: true, assetByteLength: 0 }) + }) + + it('describes the whole asset on every chunk, not the chunk', async () => { + const script = bundle.assets.find((asset) => asset.path.endsWith('.js'))! + + const middle = await chunk({ + buildId: bundle.buildId, + path: script.path, + offset: MOBILE_WEB_BUNDLE_CHUNK_BYTES + }) + + const body = MobileWebBundleChunkResultSchema.parse(middle.ok && middle.result) + expect(body.assetByteLength).toBe(script.byteLength) + expect(body.sha256).toBe(script.sha256) + expect(Buffer.from(body.dataBase64, 'base64').byteLength).toBe(MOBILE_WEB_BUNDLE_CHUNK_BYTES) + }) + + it('refuses a path that is not a manifest member', async () => { + const attempts = [ + 'assets/does-not-exist.js', + 'manifest.json', + 'index.htm', + 'assets', + 'INDEX.HTML' + ] + + for (const path of attempts) { + const response = await chunk({ buildId: bundle.buildId, path, offset: 0 }) + expect(errorMessage(response)).toBe('mobile_web_bundle_asset_unknown') + } + }) + + it('rejects a traversal path at the params schema, before any lookup', async () => { + const response = await chunk({ buildId: bundle.buildId, path: '../../etc/passwd', offset: 0 }) + + expect(response.ok).toBe(false) + expect(errorMessage(response)).not.toBe('mobile_web_bundle_asset_unknown') + }) + + it('refuses an offset that does not address a chunk boundary', async () => { + const script = bundle.assets.find((asset) => asset.path.endsWith('.js'))! + + for (const offset of [1, 1024, MOBILE_WEB_BUNDLE_CHUNK_BYTES - 1, 49_153]) { + const response = await chunk({ buildId: bundle.buildId, path: script.path, offset }) + expect(errorMessage(response)).toBe('mobile_web_bundle_offset_invalid') + } + }) + + it('refuses an aligned offset that starts past the end of the asset', async () => { + const index = bundle.assets.find((asset) => asset.path === 'index.html')! + expect(index.byteLength).toBeLessThan(MOBILE_WEB_BUNDLE_CHUNK_BYTES) + + const response = await chunk({ + buildId: bundle.buildId, + path: index.path, + offset: MOBILE_WEB_BUNDLE_CHUNK_BYTES + }) + + expect(errorMessage(response)).toBe('mobile_web_bundle_offset_invalid') + }) + + it('refuses a buildId that is not the one it is serving', async () => { + const response = await chunk({ + buildId: '0'.repeat(64), + path: 'index.html', + offset: 0 + }) + + expect(errorMessage(response)).toBe('mobile_web_bundle_build_changed') + }) + + // The auto-update case: the desktop replaced the bundle between the client's manifest call and + // its next chunk. The client must be told to restart from the manifest, not that its path is + // gone, so this is checked before the asset lookup. + it('refuses the old buildId after the install swaps bundles mid-download', async () => { + const script = bundle.assets.find((asset) => asset.path.endsWith('.js'))! + expect((await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 })).ok).toBe(true) + + rmSync(join(scratch, 'out', 'mobile-web'), { recursive: true, force: true }) + const replacement = writeSyntheticMobileWebBundle(join(scratch, 'out', 'mobile-web'), 2) + resetBundledMobileWebBundleCacheForTests() + expect(replacement.buildId).not.toBe(bundle.buildId) + + const stale = await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 }) + + expect(errorMessage(stale)).toBe('mobile_web_bundle_build_changed') + }) + + // index.html is the one path a rebuild keeps, so a verdict keyed by path alone would carry build + // A's `false` onto build B's honest file and refuse it for the life of the process. + it('does not carry a failed verdict from one build onto the next build of the same path', async () => { + writeFileSync(join(bundle.root, 'index.html'), mobileWebBundleFiller(640, 99)) + expect( + errorMessage(await chunk({ buildId: bundle.buildId, path: 'index.html', offset: 0 })) + ).toBe('mobile_web_bundle_asset_changed') + + rmSync(join(scratch, 'out', 'mobile-web'), { recursive: true, force: true }) + const replacement = writeSyntheticMobileWebBundle(join(scratch, 'out', 'mobile-web'), 8) + resetBundledMobileWebBundleCacheForTests() + + const response = await chunk({ buildId: replacement.buildId, path: 'index.html', offset: 0 }) + + expect(errorMessage(response)).toBeUndefined() + }) + + it('refuses an asset whose bytes on disk no longer hash to the manifest', async () => { + const script = bundle.assets.find((asset) => asset.path.endsWith('.js'))! + writeFileSync(join(bundle.root, script.path), mobileWebBundleFiller(script.byteLength, 99)) + + const response = await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 }) + + expect(errorMessage(response)).toBe('mobile_web_bundle_asset_changed') + }) + + // A dev rebuild under a live runtime, or a permissions change, reaches the filesystem after the + // verdict is already cached. The client must still land inside the six codes, and the host's + // absolute install path must not ride out on the reply. + it('answers a changed asset, not the filesystem error, when the asset is gone after its verdict', async () => { + const script = bundle.assets.find((asset) => asset.path.endsWith('.js'))! + expect((await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 })).ok).toBe(true) + unlinkSync(join(bundle.root, script.path)) + + const response = await chunk({ + buildId: bundle.buildId, + path: script.path, + offset: MOBILE_WEB_BUNDLE_CHUNK_BYTES + }) + + expect(errorMessage(response)).toBe('mobile_web_bundle_asset_changed') + expect(console.warn).toHaveBeenCalled() + }) + + // The only way a positional read on a regular file comes back short: the file was truncated after + // its verdict was cached. Answering the short chunk would page the client past the truncation. + it('answers a changed asset when the file is shorter than the manifest promised', async () => { + const script = bundle.assets.find((asset) => asset.path.endsWith('.js'))! + expect((await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 })).ok).toBe(true) + truncateSync(join(bundle.root, script.path), 100) + + const response = await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 }) + + expect(errorMessage(response)).toBe('mobile_web_bundle_asset_changed') + }) + + // Deliberate: a packaged bundle is immutable for the life of the install, so the verdict is worth + // one hash per asset rather than one per 48 KiB. Restoring the bytes without restarting is a dev + // scenario, and it stays refused until the process does. + it('remembers the verdict, so one hash per asset covers every later chunk', async () => { + const script = bundle.assets.find((asset) => asset.path.endsWith('.js'))! + const corrupted = mobileWebBundleFiller(script.byteLength, 99) + writeFileSync(join(bundle.root, script.path), corrupted) + expect( + errorMessage(await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 })) + ).toBe('mobile_web_bundle_asset_changed') + + writeFileSync(join(bundle.root, script.path), mobileWebBundleFiller(script.byteLength, 1)) + + expect( + errorMessage(await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 })) + ).toBe('mobile_web_bundle_asset_changed') + resetMobileWebBundleAssetVerdictsForTests() + expect((await chunk({ buildId: bundle.buildId, path: script.path, offset: 0 })).ok).toBe(true) + }) + + it('charges reads to the connection, and refuses one past the cap', async () => { + const held = Array.from({ length: MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS }, () => + acquireMobileWebBundleReadSlot('conn-a') + ) + expect(held.every((release) => release !== null)).toBe(true) + + const refused = await chunk( + { buildId: bundle.buildId, path: 'index.html', offset: 0 }, + { + connectionId: 'conn-a' + } + ) + const other = await chunk( + { buildId: bundle.buildId, path: 'index.html', offset: 0 }, + { + connectionId: 'conn-b' + } + ) + + expect(errorMessage(refused)).toBe('mobile_web_bundle_read_limited') + // One phone at its cap must not cost another phone a thing. + expect(other.ok).toBe(true) + + held[0]!() + expect( + ( + await chunk( + { buildId: bundle.buildId, path: 'index.html', offset: 0 }, + { + connectionId: 'conn-a' + } + ) + ).ok + ).toBe(true) + }) + + // Off the E2EE channel the bucket key is the device's pairing token, so a map that never drops a + // key retains one credential per socket, and reconnect churn is normal on mobile. + it('keeps no bucket for a connection that finished its reads', () => { + for (let socket = 0; socket < 50; socket++) { + const release = acquireMobileWebBundleReadSlot(`device-token-${String(socket)}`) + expect(release).not.toBeNull() + release?.() + } + + expect(mobileWebBundleReadBucketCountForTests()).toBe(0) + }) + + // connectionId is set only for E2EE mobile sockets, so the device token is what keeps a + // plain-WebSocket phone from sharing one unbounded bucket with every other caller. + it('falls back to the device token when the connection has no id', async () => { + const held = Array.from({ length: MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS }, () => + acquireMobileWebBundleReadSlot('device-token-1') + ) + expect(held.every((release) => release !== null)).toBe(true) + + const refused = await chunk( + { buildId: bundle.buildId, path: 'index.html', offset: 0 }, + { + clientId: 'device-token-1' + } + ) + + expect(errorMessage(refused)).toBe('mobile_web_bundle_read_limited') + }) + + it('stops before reading anything for a client that already disconnected', async () => { + const controller = new AbortController() + controller.abort() + + const response = await chunk( + { buildId: bundle.buildId, path: 'index.html', offset: 0 }, + { + signal: controller.signal + } + ) + + expect(response.ok).toBe(false) + expect(errorMessage(response)).toBe('client_disconnected') + }) + + it('gives the slot back after an abort, so the cap does not leak', async () => { + const controller = new AbortController() + controller.abort() + await chunk( + { buildId: bundle.buildId, path: 'index.html', offset: 0 }, + { + connectionId: 'conn-c', + signal: controller.signal + } + ) + + const held = Array.from({ length: MAX_CONCURRENT_MOBILE_WEB_BUNDLE_READS }, () => + acquireMobileWebBundleReadSlot('conn-c') + ) + + expect(held.every((release) => release !== null)).toBe(true) + }) +}) + +describe('where the resolver probes', () => { + it('finds out/mobile-web under the install root', () => { + const bundle = writeSyntheticMobileWebBundle(join(scratch, 'out', 'mobile-web'), 5) + + expect(getBundledMobileWebBundleRoot()).toBe(bundle.root) + }) + + it('answers undefined when neither layout holds a manifest', () => { + expect(getBundledMobileWebBundleRoot()).toBeUndefined() + }) + + // Unpacked electron-vite entrypoints set appPath to out/main, next to the bundle. + it('finds the bundle beside an out/main app path', async () => { + const bundle = writeSyntheticMobileWebBundle(join(scratch, 'out', 'mobile-web'), 3) + installMobileWebBundleAppPath(join(scratch, 'out', 'main')) + resetBundledMobileWebBundleCacheForTests() + + const response = await call('mobileWeb.bundle.manifest') + + expect( + MobileWebBundleManifestResultSchema.parse(response.ok && response.result).manifest.buildId + ).toBe(bundle.buildId) + }) +}) + +describe('an install with no mobile web bundle', () => { + it('reports both methods unavailable rather than failing some other way', async () => { + const manifest = await call('mobileWeb.bundle.manifest') + const body = await chunk({ buildId: '0'.repeat(64), path: 'index.html', offset: 0 }) + + expect(errorMessage(manifest)).toBe('mobile_web_bundle_unavailable') + expect(errorMessage(body)).toBe('mobile_web_bundle_unavailable') + }) + + it('reads a manifest that does not match the contract as no bundle at all', async () => { + const root = join(scratch, 'out', 'mobile-web') + writeSyntheticMobileWebBundle(root, 4) + writeFileSync(join(root, 'manifest.json'), '{"schemaVersion":2}', 'utf8') + resetBundledMobileWebBundleCacheForTests() + + const response = await call('mobileWeb.bundle.manifest') + + expect(errorMessage(response)).toBe('mobile_web_bundle_unavailable') + expect(console.warn).toHaveBeenCalled() + }) + + it('reads an unparseable manifest as no bundle at all', async () => { + const root = join(scratch, 'out', 'mobile-web') + mkdirSync(root, { recursive: true }) + writeFileSync(join(root, 'manifest.json'), 'not json', 'utf8') + resetBundledMobileWebBundleCacheForTests() + + expect(errorMessage(await call('mobileWeb.bundle.manifest'))).toBe( + 'mobile_web_bundle_unavailable' + ) + }) +}) + +// Registration in ALL_RPC_METHODS is pinned by the generated params catalog; authorization is not, +// and the mobile scanner only checks used ⊆ allowlist, so no mobile caller exists to miss these +// until A5 ships one. +describe('mobile authorization', () => { + it('lets a paired phone call both bundle methods', () => { + expect(MOBILE_RPC_METHOD_ALLOWLIST.has(MOBILE_WEB_BUNDLE_MANIFEST_METHOD)).toBe(true) + expect(MOBILE_RPC_METHOD_ALLOWLIST.has(MOBILE_WEB_BUNDLE_CHUNK_METHOD)).toBe(true) + }) +}) + +// fs.read may answer short of the window before EOF, so one call proves nothing; every other +// positional reader in the repo fills the window first, and a client must never be handed a short +// chunk because the kernel felt like splitting one. +describe('filling a read window', () => { + const source = mobileWebBundleFiller(64, 3) + + /** Answers `pieces[n]` bytes to the nth read, so a split window can be driven exactly. */ + function reader(pieces: number[]) { + const calls: number[] = [] + let piece = 0 + const read = async (buffer: Buffer, into: number, length: number, position: number) => { + calls.push(length) + const bytesRead = Math.min(pieces[piece++] ?? 0, length) + source.copy(buffer, into, position, position + bytesRead) + return { bytesRead } + } + return { calls, read } + } + + it('reads again when a read answers short of the window', async () => { + const buffer = Buffer.alloc(64) + const stub = reader([24, 40]) + + const filled = await fillMobileWebBundleReadWindow(stub, buffer, 64, 0) + + expect(filled).toBe(64) + expect(stub.calls).toEqual([64, 40]) + expect(buffer.equals(source)).toBe(true) + }) + + it('stops at the read that returns nothing, which is the truncation the caller reports', async () => { + const stub = reader([24, 0]) + + const filled = await fillMobileWebBundleReadWindow(stub, Buffer.alloc(64), 64, 0) + + expect(filled).toBe(24) + }) +}) diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle.ts b/src/main/runtime/rpc/methods/mobile-web-bundle.ts new file mode 100644 index 00000000000..780da513287 --- /dev/null +++ b/src/main/runtime/rpc/methods/mobile-web-bundle.ts @@ -0,0 +1,145 @@ +/** + * Serves this install's mobile web bundle to the paired client over the already-authenticated RPC + * connection: one call for the manifest, then one call per 48 KiB chunk of each asset. + * + * No SSH or relay proxying, ever. The bundle is an artifact of the desktop the phone paired with, + * not something a remote execution host owns, so a runtime answers only out of its own install and + * never forwards these methods to another host. + * + * `asContractError` is a total catch over the verify-and-read block: every host-side failure in + * there, whatever its cause, reaches the client as `mobile_web_bundle_asset_changed`. + */ +import { + MOBILE_WEB_BUNDLE_CHUNK_BYTES, + MOBILE_WEB_BUNDLE_CHUNK_METHOD, + MOBILE_WEB_BUNDLE_MANIFEST_METHOD, + MobileWebBundleChunkParamsSchema, + type MobileWebBundleChunkResult, + type MobileWebBundleErrorCode, + type MobileWebBundleManifestResult +} from '../../../../shared/mobile-web-bundle/bundle-rpc-contract' +import type { MobileWebBundleAsset } from '../../../../shared/mobile-web-bundle/manifest-contract' +import { + loadBundledMobileWebBundle, + type BundledMobileWebBundle +} from '../../bundled-mobile-web-bundle' +import { isClientDisconnectedError } from '../../orca-runtime-core' +import { defineMethod, InvalidArgumentError, type RpcContext } from '../core' +import { + readMobileWebBundleAssetChunk, + verifyMobileWebBundleAsset +} from './mobile-web-bundle-asset-reader' +import { + acquireMobileWebBundleReadSlot, + mobileWebBundleReadBucket +} from './mobile-web-bundle-read-admission' + +/** The code IS the message: `InvalidArgumentError` carries no data field, so the message is the only + * place a stable code can travel, and a client must be able to branch without matching prose. */ +function bundleError(code: MobileWebBundleErrorCode): InvalidArgumentError { + return new InvalidArgumentError(code) +} + +function requireBundle(): BundledMobileWebBundle { + const bundle = loadBundledMobileWebBundle() + if (!bundle) { + throw bundleError('mobile_web_bundle_unavailable') + } + return bundle +} + +function abortIfDisconnected(ctx: RpcContext): void { + if (ctx.signal?.aborted) { + throw new Error('client_disconnected') + } +} + +/** Every other way a read can fail — the asset unlinked, unreadable, or shorter than the manifest + * promised — is one thing to a client: this bundle no longer matches the manifest it was handed. + * The host path stays on the host; the reply carries only the code. */ +function asContractError(error: unknown, path: string): unknown { + if (error instanceof InvalidArgumentError || isClientDisconnectedError(error)) { + return error + } + console.warn(`[mobile-web-bundle] read failed for ${path}:`, error) + return bundleError('mobile_web_bundle_asset_changed') +} + +/** Exact match against a manifest member. `path` is never joined, normalised, or prefix-matched, so + * traversal is not mitigated here — it is unreachable. */ +function findAsset(bundle: BundledMobileWebBundle, path: string): MobileWebBundleAsset { + const asset = bundle.manifest.assets.find((candidate) => candidate.path === path) + if (!asset) { + throw bundleError('mobile_web_bundle_asset_unknown') + } + return asset +} + +/** Alignment is against the size the manifest reply advertised, which the contract deliberately + * leaves off `offset` so the host can shrink the chunk without a client release. Offset 0 is always + * in range, so a zero-byte asset is still fetchable and still reports eof. */ +function assertOffsetAddressesAChunk(offset: number, asset: MobileWebBundleAsset): void { + if (offset % MOBILE_WEB_BUNDLE_CHUNK_BYTES !== 0) { + throw bundleError('mobile_web_bundle_offset_invalid') + } + if (offset > 0 && offset >= asset.byteLength) { + throw bundleError('mobile_web_bundle_offset_invalid') + } +} + +export const MOBILE_WEB_BUNDLE_METHODS = [ + defineMethod({ + name: MOBILE_WEB_BUNDLE_MANIFEST_METHOD, + params: null, + handler: async (): Promise => ({ + manifest: requireBundle().manifest, + chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES + }) + }), + defineMethod({ + name: MOBILE_WEB_BUNDLE_CHUNK_METHOD, + params: MobileWebBundleChunkParamsSchema, + handler: async (params, ctx): Promise => { + const bundle = requireBundle() + // Checked before the asset lookup: a desktop that auto-updated mid-download must tell the + // client to restart from the manifest, not that its path went missing. + if (params.buildId !== bundle.manifest.buildId) { + throw bundleError('mobile_web_bundle_build_changed') + } + const asset = findAsset(bundle, params.path) + assertOffsetAddressesAChunk(params.offset, asset) + + const release = acquireMobileWebBundleReadSlot(mobileWebBundleReadBucket(ctx)) + if (!release) { + throw bundleError('mobile_web_bundle_read_limited') + } + try { + abortIfDisconnected(ctx) + if (!(await verifyMobileWebBundleAsset(bundle.root, bundle.manifest.buildId, asset))) { + throw bundleError('mobile_web_bundle_asset_changed') + } + abortIfDisconnected(ctx) + const data = await readMobileWebBundleAssetChunk( + bundle.root, + asset, + params.offset, + MOBILE_WEB_BUNDLE_CHUNK_BYTES + ) + return { + buildId: bundle.manifest.buildId, + path: asset.path, + offset: params.offset, + // The whole asset's length and hash, so one chunk describes the asset it belongs to. + assetByteLength: asset.byteLength, + sha256: asset.sha256, + dataBase64: data.toString('base64'), + eof: params.offset + data.byteLength >= asset.byteLength + } + } catch (error) { + throw asContractError(error, asset.path) + } finally { + release() + } + } + }) +] diff --git a/src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-allowlist.ts b/src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-allowlist.ts index ada9d7f0155..1bd86b2de28 100644 --- a/src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-allowlist.ts +++ b/src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-allowlist.ts @@ -175,6 +175,8 @@ export const MOBILE_RPC_METHOD_ALLOWLIST = new Set([ 'linear.updateIssue', 'markdown.readTab', 'markdown.saveTab', + 'mobileWeb.bundle.chunk', + 'mobileWeb.bundle.manifest', 'notifications.getMissedSince', 'notifications.registerPush', 'notifications.subscribe', diff --git a/src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts b/src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts index 4cc356026f3..a4c1abddbb2 100644 --- a/src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts +++ b/src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts @@ -10,7 +10,6 @@ import { MobileWebBundleChunkParamsSchema, MobileWebBundleChunkResultSchema, MobileWebBundleErrorCodeSchema, - MobileWebBundleManifestParamsSchema, MobileWebBundleManifestResultSchema, MOBILE_WEB_BUNDLE_CHUNK_BYTES, MOBILE_WEB_BUNDLE_CHUNK_METHOD, @@ -85,11 +84,6 @@ describe('MobileWebBundleErrorCodeSchema', () => { }) describe('mobileWeb.bundle.manifest payloads', () => { - it('takes null params', () => { - expect(MobileWebBundleManifestParamsSchema.safeParse(null).success).toBe(true) - expect(MobileWebBundleManifestParamsSchema.safeParse({}).success).toBe(false) - }) - it('carries a parsed manifest and the advertised chunk size', () => { const reply = { manifest: VALID_MANIFEST, chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES } const parsed = MobileWebBundleManifestResultSchema.safeParse(reply) diff --git a/src/shared/mobile-web-bundle/bundle-rpc-contract.ts b/src/shared/mobile-web-bundle/bundle-rpc-contract.ts index 08b4aa88a72..4395a4e9d1c 100644 --- a/src/shared/mobile-web-bundle/bundle-rpc-contract.ts +++ b/src/shared/mobile-web-bundle/bundle-rpc-contract.ts @@ -10,6 +10,8 @@ import { * against the 1 MiB frame ceiling on both the WebSocket and relay transports. */ export const MOBILE_WEB_BUNDLE_CHUNK_BYTES = 48 * 1024 +/** Takes no params, and carries no params schema: the dispatcher substitutes `{}` for absent params, + * so a `z.null()` schema could never be satisfied. The method declares `params: null` host-side. */ export const MOBILE_WEB_BUNDLE_MANIFEST_METHOD = 'mobileWeb.bundle.manifest' export const MOBILE_WEB_BUNDLE_CHUNK_METHOD = 'mobileWeb.bundle.chunk' @@ -36,8 +38,6 @@ export const MOBILE_WEB_BUNDLE_ERROR_CODES = hostUnionArms export type MobileWebBundleManifestResult = z.infer export type MobileWebBundleChunkParams = z.infer export type MobileWebBundleChunkResult = z.infer diff --git a/src/shared/rpc-contract/rpc-params-catalog.generated.ts b/src/shared/rpc-contract/rpc-params-catalog.generated.ts index 3168406805c..7278e5eda8e 100644 --- a/src/shared/rpc-contract/rpc-params-catalog.generated.ts +++ b/src/shared/rpc-contract/rpc-params-catalog.generated.ts @@ -22,6 +22,7 @@ import { PairingGetEndpointsParamsSchema, PairingProvisionRelayParamsSchema } from '../mobile-relay-credential-contract' +import { MobileWebBundleChunkParamsSchema } from '../mobile-web-bundle/bundle-rpc-contract' import { pluginConsentRequestSchema } from '../plugins/plugin-consent-request' import { AccountsUnsubscribeParams, @@ -957,6 +958,8 @@ export const RPC_PARAMS_BY_METHOD = { 'linear.updateIssue': IssueUpdateOfLinearParams, 'markdown.readTab': ActivateTab, 'markdown.saveTab': SaveMarkdownTab, + 'mobileWeb.bundle.chunk': MobileWebBundleChunkParamsSchema, + 'mobileWeb.bundle.manifest': null, 'nativeChat.readSession': NativeChatSession, 'nativeChat.subscribe': NativeChatSession, 'nativeChat.unsubscribe': NativeChatUnsubscribe, From ffc812cdce619d4f96871b1a6b3ae83f8f1152ee Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:12:16 -0700 Subject: [PATCH 53/59] Reveal active workspaces with minimal filter changes (#21364) * Reveal workspaces by adjusting only blocking filters * Update runtime localization catalog * Preserve minimal reveal behavior across catalogs and folders --- .../src/components/sidebar/WorktreeList.tsx | 6 +- .../worktree-list/listing/use-filters.ts | 96 ++++++++++++++++++- .../navigation/use-reveal-requests.test.tsx | 26 +++-- .../navigation/use-reveal-requests.ts | 31 ++++-- .../src/i18n/en-runtime-required.json | 14 +-- src/renderer/src/i18n/locales/en.json | 4 +- 6 files changed, 151 insertions(+), 26 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index ce6813d229f..d2f13e524ce 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -104,7 +104,8 @@ const WorktreeList = React.memo(function WorktreeList({ ) const agentSendTargetWorktreeId = useAgentSendTargetWorktreeId() - const { filterState, hasFilters, clearFilters } = useSidebarWorktreeFilters() + const { filterState, hasFilters, clearFilters, revealWorkspaceFilters } = + useSidebarWorktreeFilters() const sortedIds = useSidebarWorktreeSortOrder({ allWorktrees, repoMap, sortBy }) const manualOrderCatalog = useMemo( () => buildWorktreeManualOrderCatalog({ worktrees: allWorktrees, folderWorkspaces }), @@ -244,7 +245,8 @@ const WorktreeList = React.memo(function WorktreeList({ worktrees: allWorktrees, folderWorkspaces, hasFilters, - clearFilters + clearFilters, + revealWorkspaceFilters }) const filtersHideAllRows = shouldFiltersHideAllRows({ diff --git a/src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts b/src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts index f04e57194af..37bc57be8de 100644 --- a/src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts +++ b/src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts @@ -1,7 +1,30 @@ import { useCallback, useMemo } from 'react' import { useAppStore } from '@/store' import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../../../shared/constants' -import { computeClearFilterActions, sidebarHasActiveFilters } from '../../visible-worktrees' +import { + computeClearFilterActions, + sidebarHasActiveFilters, + isAutomationGeneratedWorkspace, + isCliCreatedWorkspace, + isDetachedHeadWorkspace, + isSleepingSweepExemptWorkspace +} from '../../visible-worktrees' +import type { Worktree } from '../../../../../../shared/worktree/types' +import { + getWorktreeExecutionHostId, + getSettingsFocusedExecutionHostId +} from '../../../../../../shared/execution-host' +import { isDefaultBranchWorkspace } from '../../default-branch-workspace' +import { + getPairedDeviceIdsByEnvironment, + isWorkspaceFromOtherDevice +} from '../../workspace-creator-visibility' +import { getAgentStatusEpochNow } from '@/lib/agent-status-epoch-clock' +import { getWorktreeIdsWithLiveAgent, isInactiveWorkspace } from '@/lib/worktree-activity-state' +import { + getVisibleWorktreeBrowserActivityTabs, + getVisibleWorktreeTerminalActivityTabs +} from '../../visible-worktree-activity-inputs' export type SidebarWorktreeFilters = ReturnType @@ -32,6 +55,70 @@ export function useSidebarWorktreeFilters() { const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds) const setVisibleWorkspaceHostIds = useAppStore((s) => s.setVisibleWorkspaceHostIds) + const revealWorkspaceFilters = useCallback((worktree: Worktree) => { + const state = useAppStore.getState() + const repo = state.repos.find((candidate) => candidate.id === worktree.repoId) + const targetHostId = getWorktreeExecutionHostId( + worktree, + repo, + getSettingsFocusedExecutionHostId(state.settings) + ) + + if (state.filterRepoIds.length > 0 && !state.filterRepoIds.includes(worktree.repoId)) { + state.setFilterRepoIds([...state.filterRepoIds, worktree.repoId]) + } + const visibleHostIds = state.visibleWorkspaceHostIds + const scopedHostIds = + visibleHostIds ?? (state.workspaceHostScope === 'all' ? null : [state.workspaceHostScope]) + if (scopedHostIds && !scopedHostIds.includes(targetHostId)) { + state.setVisibleWorkspaceHostIds([...scopedHostIds, targetHostId]) + } + if (state.hideDefaultBranchWorkspace && isDefaultBranchWorkspace(worktree)) { + state.setHideDefaultBranchWorkspace(false) + } + if (state.hideAutomationGeneratedWorkspaces && isAutomationGeneratedWorkspace(worktree)) { + state.setHideAutomationGeneratedWorkspaces(false) + } + if (state.hideCliCreatedWorkspaces && isCliCreatedWorkspace(worktree)) { + state.setHideCliCreatedWorkspaces(false) + } + if (state.hideDetachedHeadWorkspaces && isDetachedHeadWorkspace(worktree)) { + state.setHideDetachedHeadWorkspaces(false) + } + if (state.hideWorkspacesFromOtherDevices) { + const pairedDeviceIds = getPairedDeviceIdsByEnvironment( + state.runtimeEnvironments, + state.runtimeStatusByEnvironmentId + ) + if (isWorkspaceFromOtherDevice(worktree, pairedDeviceIds)) { + state.setHideWorkspacesFromOtherDevices(false) + } + } + if (!state.showSleepingWorkspaces) { + const tabsByWorktree = getVisibleWorktreeTerminalActivityTabs(state.tabsByWorktree) + const browserTabsByWorktree = getVisibleWorktreeBrowserActivityTabs( + state.browserTabsByWorktree + ) + const liveAgentWorktrees = getWorktreeIdsWithLiveAgent( + state.agentStatusByPaneKey, + tabsByWorktree, + getAgentStatusEpochNow(state.agentStatusEpoch) + ) + if ( + !isSleepingSweepExemptWorkspace(worktree, state.alwaysShowDefaultBranchWorkspace) && + isInactiveWorkspace( + worktree.id, + tabsByWorktree, + state.ptyIdsByTabId, + browserTabsByWorktree, + liveAgentWorktrees + ) + ) { + state.setShowSleepingWorkspaces(true) + } + } + }, []) + // Why: count hideDefaultBranchWorkspace as a filter so the Clear Filters escape hatch stays reachable when it alone empties the list. const filterState = useMemo( () => ({ @@ -102,5 +189,10 @@ export function useSidebarWorktreeFilters() { filterState ]) - return { filterState, hasFilters: sidebarHasActiveFilters(filterState), clearFilters } + return { + filterState, + hasFilters: sidebarHasActiveFilters(filterState), + clearFilters, + revealWorkspaceFilters + } } diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx index f31b2bc9437..36b86ab0e9a 100644 --- a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx @@ -79,6 +79,7 @@ beforeEach(() => { sortOrder: 1, lastActivityAt: 1 } + const clearFilters = vi.fn() args = { groupBy: 'repo', renderedSidebarRowKeys: new Set(), @@ -90,7 +91,8 @@ beforeEach(() => { worktrees: [worktree], folderWorkspaces: [], hasFilters: true, - clearFilters: vi.fn() + clearFilters, + revealWorkspaceFilters: clearFilters } }) @@ -103,7 +105,9 @@ describe('revealing a filtered workspace', () => { it('explains the filter reset and leaves filters intact when dismissed', async () => { await render() await act(async () => requestScrollToCurrentWorkspaceReveal()) - expect(document.body.textContent).toContain('Revealing it will clear your sidebar filters.') + expect(document.body.textContent).toContain( + 'Revealing it will adjust only the filters hiding it.' + ) expect(args.clearFilters).not.toHaveBeenCalled() expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled() await click('Keep filters') @@ -111,13 +115,23 @@ describe('revealing a filtered workspace', () => { expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled() }) + it('delegates to the minimal filter revealer when provided', async () => { + const revealWorkspaceFilters = vi.fn() + args = { ...args, revealWorkspaceFilters } + await render() + await act(async () => requestScrollToCurrentWorkspaceReveal()) + await click('Adjust filters and reveal') + expect(revealWorkspaceFilters).toHaveBeenCalledWith(args.worktrees[0]) + expect(args.clearFilters).not.toHaveBeenCalled() + }) + it('clears filters and reveals on the original execution host only after confirmation', async () => { await render() await act(async () => { requestScrollToCurrentWorkspaceReveal() requestScrollToCurrentWorkspaceReveal() }) - await click('Clear filters and reveal') + await click('Adjust filters and reveal') expect(args.clearFilters).toHaveBeenCalledTimes(1) expect(state.revealWorktreeInSidebar).toHaveBeenCalledWith('wt-1', { behavior: 'smooth', @@ -174,7 +188,7 @@ describe('revealing a filtered workspace', () => { await act(async () => requestScrollToCurrentWorkspaceReveal()) args = { ...args, visibleWorktrees: args.worktrees } await render() - await click('Clear filters and reveal') + await click('Adjust filters and reveal') expect(args.clearFilters).not.toHaveBeenCalled() expect(state.revealWorktreeInSidebar).toHaveBeenCalledTimes(1) }) @@ -184,7 +198,7 @@ describe('revealing a filtered workspace', () => { await act(async () => requestScrollToCurrentWorkspaceReveal()) args = { ...args, currentSidebarWorktreeId: 'wt-2' } await render() - await click('Clear filters and reveal') + await click('Adjust filters and reveal') expect(args.clearFilters).not.toHaveBeenCalled() expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled() }) @@ -219,7 +233,7 @@ describe('revealing a filtered workspace', () => { await act(async () => requestScrollToCurrentWorkspaceRevealAndRename()) expect(args.clearFilters).not.toHaveBeenCalled() if (filtered) { - await click('Clear filters and reveal') + await click('Adjust filters and reveal') expect(args.clearFilters).toHaveBeenCalledTimes(1) } else { expect(document.querySelector('[role="dialog"]')).toBeNull() diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts index 33841f51b6c..ebe5d695b6d 100644 --- a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts @@ -41,6 +41,7 @@ export function useSidebarRevealRequests(args: { folderWorkspaces: readonly FolderWorkspace[] hasFilters: boolean clearFilters: () => void + revealWorkspaceFilters: (worktree: Worktree) => void }): void { const { groupBy, @@ -53,7 +54,8 @@ export function useSidebarRevealRequests(args: { worktrees, folderWorkspaces, hasFilters, - clearFilters + clearFilters, + revealWorkspaceFilters } = args const setGroupBy = useAppStore((s) => s.setGroupBy) const pendingRevealSidebarRow = useAppStore((s) => s.pendingRevealSidebarRow) @@ -80,15 +82,29 @@ export function useSidebarRevealRequests(args: { return } if (!renderedSidebarRowKeys.has(rowKey) && hasFilters) { - clearFilters() + const target = getKnownSidebarWorktreeById( + rowKey, + worktreeMap, + folderWorkspaces, + worktrees, + currentSidebarExecutionHostId + ) + if (target) { + revealWorkspaceFilters(target) + } } }, [ clearFilters, groupBy, hasFilters, + currentSidebarExecutionHostId, + folderWorkspaces, pendingRevealSidebarRow, renderedSidebarRowKeys, - setGroupBy + setGroupBy, + worktreeMap, + worktrees, + revealWorkspaceFilters ]) const handleRevealCurrentWorkspaceRequest = useCallback( @@ -139,9 +155,9 @@ export function useSidebarRevealRequests(args: { title: translate('sidebar.revealFiltered.title', 'Reveal hidden workspace?'), description: translate( 'sidebar.revealFiltered.description', - 'The active workspace is hidden in the sidebar. Revealing it will clear your sidebar filters.' + 'The active workspace is hidden in the sidebar. Revealing it will adjust only the filters hiding it.' ), - confirmLabel: translate('sidebar.revealFiltered.confirm', 'Clear filters and reveal'), + confirmLabel: translate('sidebar.revealFiltered.confirm', 'Adjust filters and reveal'), cancelLabel: translate('sidebar.revealFiltered.cancel', 'Keep filters') }) } finally { @@ -164,7 +180,7 @@ export function useSidebarRevealRequests(args: { latest.visibleFolderWorkspaces ) ) { - latest.clearFilters() + revealWorkspaceFilters(activeWorktree) } } revealWorktreeInSidebar(currentSidebarWorktreeId, { @@ -185,7 +201,8 @@ export function useSidebarRevealRequests(args: { visibleFolderWorkspaces, revealWorktreeInSidebar, worktreeMap, - worktrees + worktrees, + revealWorkspaceFilters ] ) diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 5e1c5bb3a84..8fe443f65de 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -73,19 +73,19 @@ "f5a6b38a14": "sheet" }, "NativeChatResumeOnRestartModal": { + "continueRefused_one": "{{value0}} chat could not be continued. Open it to continue manually.", + "continueRefused_other": "{{value0}} chats could not be continued. Open them to continue manually.", + "continueUnconfirmed_one": "Continuation delivery is unconfirmed for {{value0}} chat. Open it to check before sending another message.", + "continueUnconfirmed_other": "Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.", "manyAgents": "{{value0}} agents", "oneAgent": "1 agent", "projects": "Folder workspaces", "reconnectAgent": "Reconnect {{value0}} chat", - "resume": "Reconnect", - "continueUnconfirmed_one": "Continuation delivery is unconfirmed for {{value0}} chat. Open it to check before sending another message.", - "continueUnconfirmed_other": "Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.", + "reconnectRefused_one": "{{value0}} chat could not be reconnected. You can still open it normally.", + "reconnectRefused_other": "{{value0}} chats could not be reconnected. You can still open them normally.", "reconnectUnconfirmed_one": "Reconnection is unconfirmed for {{value0}} chat. You can still open it normally.", "reconnectUnconfirmed_other": "Reconnection is unconfirmed for {{value0}} chats. You can still open them normally.", - "continueRefused_one": "{{value0}} chat could not be continued. Open it to continue manually.", - "continueRefused_other": "{{value0}} chats could not be continued. Open them to continue manually.", - "reconnectRefused_one": "{{value0}} chat could not be reconnected. You can still open it normally.", - "reconnectRefused_other": "{{value0}} chats could not be reconnected. You can still open them normally." + "resume": "Reconnect" }, "NewWorkspaceComposerCard": { "0e587e31fb": "yaml", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 00c2bfe2514..6ded01db780 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -2,8 +2,8 @@ "sidebar": { "revealFiltered": { "title": "Reveal hidden workspace?", - "description": "The active workspace is hidden in the sidebar. Revealing it will clear your sidebar filters.", - "confirm": "Clear filters and reveal", + "description": "The active workspace is hidden in the sidebar. Revealing it will adjust only the filters hiding it.", + "confirm": "Adjust filters and reveal", "cancel": "Keep filters" } }, From 945ea33541d8b47dc51f973aff5dff2fbc34a1ae Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:12:46 -0700 Subject: [PATCH 54/59] Revert "fix(editor): evict stale mirrored file tabs (#21363)" (#21368) This reverts commit 07e8c851b8b03651468459d9a63c285329e6e105. The eviction keys on `selector_not_found`, which this repo documents twice as UNKNOWN rather than absence: - `remote-browser-stream-errors.ts`: "it means 'I could not resolve this right now', which is UNKNOWN, not proof the target is gone. Its producer is a live worktree scan behind a 1s-TTL cache ... a slow scan can surface it transiently. Treating that as permanent would strand the pane forever, which is the exact bug this file exists to prevent." - `web-runtime-session-tab-lifecycle.ts`, added by #21277: "'selector_not_found' is a transient worktree resolver state (e.g. during scans or cache warm-up) and must not become a durable close tombstone." Two unambiguous absence codes exist for this purpose -- `tab_not_found` and `terminal_tab_not_found` -- and #21277 had just finished excluding `selector_not_found` from them. This keyed on the excluded one. Consequences, after roughly 3.75s of retries: 1. `closeFile` deletes `editorDrafts[fileId]` with no dirty check and no confirmation, so a transient resolver blip discards unsaved edits. 2. `closeFile` calls `notifyHostOfMirroredEditorClose`, so the host closes its copy too -- the eviction is not local and not recoverable. The `!ownerNotReady` guard does not cover this: `ownerNotReady` means the host is still connecting, while `selector_not_found` is emitted for a cold resolver cache or an unhydrated catalog, which is a different state. #21041 is still open. A correct fix keys on the two definitive absence codes, refuses to evict a tab that has a draft, and has a test proving a dirty mirrored tab survives `selector_not_found`. --- .../editor/useEditorPanelContentState.ts | 3 +- .../useEditorPanelFileLoadRetry.test.tsx | 32 ------------------- .../editor/useEditorPanelFileLoadRetry.ts | 19 ----------- 3 files changed, 1 insertion(+), 53 deletions(-) diff --git a/src/renderer/src/components/editor/useEditorPanelContentState.ts b/src/renderer/src/components/editor/useEditorPanelContentState.ts index 93c33d0961c..ec1a0fc6b1b 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentState.ts +++ b/src/renderer/src/components/editor/useEditorPanelContentState.ts @@ -1,6 +1,6 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react' import type { OpenFile } from '@/store/slices/editor' -import { useAppStore } from '@/store' +import type { useAppStore } from '@/store' import type { DiffContent, FileContent } from './editor-panel-content-types' import { useEditorPanelExternalContentEvents, @@ -194,7 +194,6 @@ export function useEditorPanelContentState({ fileLoadRetryAttemptsRef, loadFileContent, openFilesRef, - closeFile: useAppStore.getState().closeFile, setFileContents }) diff --git a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx index fe40142c645..6a29017d654 100644 --- a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx +++ b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx @@ -49,7 +49,6 @@ function Harness({ attemptsRef, isVisible = true, loadFileContent, - closeFile = vi.fn(), setFileContents }: { file: OpenFile @@ -57,7 +56,6 @@ function Harness({ attemptsRef: { current: Record } isVisible?: boolean loadFileContent: (filePath: string, id: string) => Promise - closeFile?: (fileId: string) => void setFileContents: ( updater: (prev: Record) => Record ) => void @@ -68,7 +66,6 @@ function Harness({ fileLoadRetryAttemptsRef: attemptsRef, loadFileContent: loadFileContent as never, openFilesRef: { current: [file] }, - closeFile, setFileContents: setFileContents as never }) return null @@ -106,35 +103,6 @@ describe('useEditorPanelFileLoadRetry — owner-not-ready bounding (#6648)', () expect(shouldRetryFileLoadError('Access denied: outside allowed directories')).toBe(false) }) - it('evicts a mirrored tab after selector resolution stays missing', () => { - const file = makeFile({ mirroredFromRuntimeSession: true }) - const attemptsRef = { current: { [file.id]: 3 } } - const closeFile = vi.fn() - const fileContents: Record = { - [file.id]: { content: '', isBinary: false, loadError: 'selector_not_found' } - } - - container = document.createElement('div') - document.body.appendChild(container) - root = createRoot(container) - act(() => { - root?.render( - undefined)} - closeFile={closeFile} - setFileContents={(updater) => { - updater(fileContents) - }} - /> - ) - }) - - expect(closeFile).toHaveBeenCalledWith(file.id) - }) - it('does not spend retry budget when hiding cancels a pending retry', () => { setTimeoutSpy.mockRestore() setTimeoutSpy = vi.spyOn(window, 'setTimeout') diff --git a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts index 57a9483218e..9fb96ad80aa 100644 --- a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts +++ b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts @@ -7,7 +7,6 @@ import { } from './editor-panel-content-types' const FILE_LOAD_RETRY_DELAYS_MS = [250, 1000, 2500] -const noopCloseFile = (): void => {} // Why: a remote host can take a while to finish connecting. The owner-not-ready // check is a pure local store read (it throws before any network call until the // SSH repo hydrates), so poll it at a steady cadence — but cap the wait so a @@ -31,14 +30,9 @@ type UseEditorPanelFileLoadRetryParams = { relativePath?: string ) => Promise openFilesRef: MutableRefObject - closeFile?: (fileId: string) => void setFileContents: Dispatch>> } -function isSelectorNotFoundError(message: string): boolean { - return message.trim().toLowerCase() === 'selector_not_found' -} - export function shouldRetryFileLoadError(message: string): boolean { // Terminal: the owner-not-ready budget is spent; only an explicit Retry should // restart it, never the automatic backoff. @@ -60,7 +54,6 @@ export function useEditorPanelFileLoadRetry({ fileLoadRetryAttemptsRef, loadFileContent, openFilesRef, - closeFile = noopCloseFile, setFileContents }: UseEditorPanelFileLoadRetryParams): void { const activeFileLoadRetryId = activeFile?.id ?? null @@ -82,16 +75,6 @@ export function useEditorPanelFileLoadRetry({ ? OWNER_NOT_READY_RETRY_LIMIT : FILE_LOAD_RETRY_DELAYS_MS.length if (retryCount >= retryLimit) { - if ( - !ownerNotReady && - isSelectorNotFoundError(activeFileLoadError) && - activeFile?.mirroredFromRuntimeSession === true - ) { - // A host-mirrored file whose worktree stays unresolvable after the normal - // read retries is stale; evict it before snapshots can select it again. - closeFile(activeFileLoadRetryId) - return - } // Why: the remote host never finished connecting. Replace the transient // "still connecting" text with a truthful terminal message so it does not // look like it is still retrying; Retry starts a fresh budget (#6648). @@ -143,8 +126,6 @@ export function useEditorPanelFileLoadRetry({ }, [ activeFileLoadRetryId, activeFileLoadError, - activeFile?.mirroredFromRuntimeSession, - closeFile, fileLoadRetryAttemptsRef, loadFileContent, openFilesRef, From 8d2f16856f1ce5f29f6f00523ff75b85431c4093 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:12:55 -0700 Subject: [PATCH 55/59] fix(session): scope agent resume to the host that captured the session (#21288) * fix(session): scope agent resume to the host that captured the session A provider session id names a transcript in one machine's agent state directory. Nothing in the resume path compared that machine against the one the resume executes on, so a record captured on host A reached a `--resume` run on host B, which answers `No conversation found with session ID`. Three things make the drift reachable: `worktreeId` is `repoId::path` with no host component, sleeping records are `'sleepingAgentKeyed'` so boot-time host-contention parking never arbitrates them and every partition merges into one map without retaining provenance, and both issuers resolve their launch target from the current catalog. Both issuers are gated. The activation sweep hands `quit`/`live` records whose pane still exists to the pane's own cold restore, so gating the sweep alone changed nothing in the SSH lane. Declines rather than guesses: the record is preserved and remains resumable by hand. A refused resume is recoverable, a forked transcript is not. The predicate fails open on anything it cannot positively rule out -- an unstamped record, an empty stamp, or a `runtime:` host, which a paired client uses to relabel its host's own SSH workspaces. The cold-restore gate consults both the pane's transport and the catalog. The transport alone was racy: it is unresolved on an early reattach frame, and that frame is exactly when a wrong resume escaped. * docs(session): name the inverted fail-open direction at the resume gate * fix(session): keep an unresolved catalog out of the resume host verdict The worktree form of the resume gate resolved the current host through getExecutionHostIdForWorktree, which answers 'local' for a worktree the catalog has no row for. Read as a host, that made every SSH-stamped record look foreign until its repo row landed, contradicting the module's own contract that it reports only a positively-known disagreement. Add getKnownExecutionHostIdForWorktree, which returns null in that silence (no repo row for a git worktree, no folder-workspace row for a folder workspace), and route the gate through it; the pair form already fails open on a null host. The routing resolver keeps its default unchanged. The CI red on the control case was a separate spec race: the ledger wait returned as soon as the ledger was non-empty, and it already held the first launch's `--version` probe, so the control read two probes and gave up before the cold-restore had typed `--resume` (the failure screenshot shows the command running in the pane). The spec now reads only the lines the relaunch appended, anchors on the relaunch's PTY binding and its own probe, and then waits for `--resume` for the control case or a bounded grace for the refusal case. --- config/scripts/run-ssh-docker-e2e.mjs | 1 + .../cold-restore-resume-startup.ts | 23 ++ ...agent-session-execution-host-scope.test.ts | 239 ++++++++++++++ .../src/lib/resume-sleeping-agent-session.ts | 9 + .../sleeping-record-execution-host-scope.ts | 78 +++++ .../src/lib/worktree-runtime-owner.test.ts | 37 +++ .../src/lib/worktree-runtime-owner.ts | 47 ++- ...-stale-resume-execution-host-scope.spec.ts | 306 ++++++++++++++++++ 8 files changed, 733 insertions(+), 7 deletions(-) create mode 100644 src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts create mode 100644 src/renderer/src/lib/sleeping-record-execution-host-scope.ts create mode 100644 tests/e2e/ssh-stale-resume-execution-host-scope.spec.ts diff --git a/config/scripts/run-ssh-docker-e2e.mjs b/config/scripts/run-ssh-docker-e2e.mjs index fc0d628ab78..2eea3575fde 100644 --- a/config/scripts/run-ssh-docker-e2e.mjs +++ b/config/scripts/run-ssh-docker-e2e.mjs @@ -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', diff --git a/src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts b/src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts index c719e4a83ef..d15231fca7d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts @@ -2,6 +2,10 @@ import { useAppStore } from '@/store' import { createBrowserUuid } from '@/lib/browser-uuid' import { buildAgentResumeStartupPlan } from '@/lib/tui-agent-startup' import { resolveAgentResumeLaunchTarget } from '@/lib/agent-resume-launch-target' +import { + agentResumeOriginNamesAnotherExecutionHost, + sleepingRecordNamesAnotherExecutionHost +} from '@/lib/sleeping-record-execution-host-scope' import { resolveTuiAgentLaunchArgs, resolveTuiAgentLaunchEnv @@ -37,6 +41,25 @@ export function bindBuildColdRestoreAgentResumeStartup(session: ConnectPanePtySe if (!providerSession) { return null } + // Why: this is the second issuer of `--resume`, and the one that handles a quit/live record + // whose pane still exists — the sweep hands those here rather than launching them. A session id + // names a transcript on the machine that captured it, so replaying one over a pane now attached + // to a different host answers `No conversation found`. Returning null leaves the pane with a + // plain shell and the record intact, which the user can resume by hand. + // + // Two sources are consulted because either can be the one that knows. `session.executionHostId` + // is the pane's own transport and is authoritative when set, but it is still unresolved on an + // early reattach frame — and failing open on that frame is precisely when a wrong resume slips + // out. The catalog's answer for the record's worktree covers that window. + if ( + agentResumeOriginNamesAnotherExecutionHost( + useLiveEntry ? entry.connectionId : sleepingRecord?.connectionId, + session.executionHostId + ) || + (sleepingRecord && sleepingRecordNamesAnotherExecutionHost(sleepingRecord, state)) + ) { + return null + } const matchingSleepingLaunchConfig = sleepingRecord?.launchConfig && (!useLiveEntry || diff --git a/src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts b/src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts new file mode 100644 index 00000000000..54b5459a8cf --- /dev/null +++ b/src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts @@ -0,0 +1,239 @@ +/** + * A provider session id names a transcript in ONE machine's agent state directory. Replaying a + * record captured on host A as a `--resume` executed on host B answers + * `No conversation found with session ID: ` at best, and at worst reopens an unrelated + * transcript that happens to share the id. + * + * Nothing in the resume path was host-scoped: `worktreeId` is `repoId::path` with no host component + * (shared/worktree/host-qualified-identity.ts), sleeping records are `'sleepingAgentKeyed'` so the + * boot-time host-contention parking never arbitrates them and every partition's records merge into + * one map, and `launchSleepingAgentSession` resolves its launch target from the *current* catalog. + * + * Both directions matter. The sweep must decline when the record names another machine, and it must + * still resume everything it cannot positively rule out — a gate that refuses on absent evidence + * would strand every record captured before the stamp existed. + */ +import { afterEach, describe, expect, it } from 'vitest' +import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' +import type { ExecutionHostId } from '../../../shared/execution-host' +import { useAppStore } from '@/store' +import { makeWorktree, TEST_REPO } from '@/store/slices/store-test-helpers' +import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session' +import { + agentResumeOriginNamesAnotherExecutionHost, + sleepingRecordNamesAnotherExecutionHost +} from './sleeping-record-execution-host-scope' +import type { WorktreeRuntimeOwnerState } from './worktree-runtime-owner' + +const initialAppStoreState = useAppStore.getState() + +const TARGET_ID = 'openclaw' +const REMOTE_PATH = '/home/neil/projects/orca-test123' +const WORKTREE_ID = `repo-1::${REMOTE_PATH}` +const SESSION_ID = '87987465-66f6-4967-bf3f-0659565cbcc5' + +afterEach(() => { + useAppStore.setState(initialAppStoreState, true) +}) + +function makeRecord( + overrides: Partial = {} +): SleepingAgentSessionRecord { + return { + paneKey: 'tab-1:leaf-1', + tabId: 'tab-1', + worktreeId: WORKTREE_ID, + agent: 'claude', + providerSession: { key: 'session_id', id: SESSION_ID }, + prompt: 'finish the task', + state: 'working', + origin: 'quit', + capturedAt: 1, + updatedAt: 1, + ...overrides + } +} + +/** A catalog that resolves WORKTREE_ID to exactly `hostId`, with no tab rows for it. */ +function catalogOwnedBy(hostId: ExecutionHostId): WorktreeRuntimeOwnerState { + const connectionId = hostId.startsWith('ssh:') + ? decodeURIComponent(hostId.slice('ssh:'.length)) + : undefined + return { + repos: [ + { + id: 'repo-1', + ...(connectionId ? { connectionId } : {}), + ...(hostId.startsWith('runtime:') ? { executionHostId: hostId } : {}) + } + ], + worktreesByRepo: { + 'repo-1': [makeWorktree({ id: WORKTREE_ID, repoId: 'repo-1', path: REMOTE_PATH, hostId })] + } + } +} + +describe('sleepingRecordNamesAnotherExecutionHost', () => { + it.each([ + ['an SSH record on a different SSH target', 'other-target', `ssh:${TARGET_ID}`], + ['an SSH record on the local host', TARGET_ID, 'local'], + ['a local-or-runtime record on an SSH host', null, `ssh:${TARGET_ID}`] + ] as const)('refuses %s', (_label, connectionId, hostId) => { + const record = makeRecord({ connectionId }) + expect(sleepingRecordNamesAnotherExecutionHost(record, catalogOwnedBy(hostId))).toBe(true) + }) + + it.each([ + ['the same SSH target', TARGET_ID, `ssh:${TARGET_ID}`], + ['a target id needing URI encoding', 'my host', 'ssh:my%20host'], + ['a local record on the local host', null, 'local'], + // A paired client renames its host's workspaces — including that host's SSH ones — into its own + // runtime namespace, so a runtime answer is no evidence about the machine holding the transcript. + ['an SSH record whose workspace now reads as a paired runtime', TARGET_ID, 'runtime:env-1'], + ['a local-or-runtime record on a paired runtime', null, 'runtime:env-1'] + ] as const)('allows %s', (_label, connectionId, hostId) => { + const record = makeRecord({ connectionId }) + expect(sleepingRecordNamesAnotherExecutionHost(record, catalogOwnedBy(hostId))).toBe(false) + }) + + it.each([ + ['never stamped', undefined], + ['stamped with whitespace', ' '] + ] as const)('fails open on a record %s', (_label, connectionId) => { + // #9030 leaves SSH orphans unstamped. Refusing on absent evidence would strand every record + // captured before the stamp existed, which is a worse failure than the one being fixed. + const record = makeRecord(connectionId === undefined ? {} : { connectionId }) + expect( + sleepingRecordNamesAnotherExecutionHost(record, catalogOwnedBy(`ssh:${TARGET_ID}`)) + ).toBe(false) + }) + + it.each([ + ['an SSH record', TARGET_ID], + ['a local-or-runtime record', null] + ] as const)( + 'fails open for %s when the catalog has no row for the worktree', + (_label, connectionId) => { + // The routing resolver answers `'local'` for a worktree it has no row for. Read as a host, that + // would make every SSH record look foreign until its repo row lands — a gate that never resumes + // yours is the inverse of the defect and worse. + const record = makeRecord({ connectionId }) + expect( + sleepingRecordNamesAnotherExecutionHost(record, { repos: [], worktreesByRepo: {} }) + ).toBe(false) + } + ) + + it('still refuses an SSH record once a repo row positively names the worktree local', () => { + const record = makeRecord({ connectionId: TARGET_ID }) + expect( + sleepingRecordNamesAnotherExecutionHost(record, { + repos: [{ id: 'repo-1' }], + worktreesByRepo: {} + }) + ).toBe(true) + }) +}) + +describe('agentResumeOriginNamesAnotherExecutionHost', () => { + // The pane cold-restore path asks the same question against the transport the pane is attached to + // rather than the catalog, so the host-pair form is exported and pinned separately. + it.each([ + ['an SSH origin against another SSH pane', TARGET_ID, 'ssh:elsewhere', true], + ['an SSH origin against a local pane', TARGET_ID, 'local', true], + ['a local origin against an SSH pane', null, `ssh:${TARGET_ID}`, true], + ['an SSH origin against its own pane', TARGET_ID, `ssh:${TARGET_ID}`, false], + ['a local origin against a local pane', null, 'local', false], + ['an SSH origin against a paired-runtime pane', TARGET_ID, 'runtime:env-1', false] + ] as const)('reports %s as %s', (_label, originConnectionId, hostId, expected) => { + expect(agentResumeOriginNamesAnotherExecutionHost(originConnectionId, hostId)).toBe(expected) + }) + + it.each([null, undefined])( + 'fails open when the pane has no resolved execution host (%s)', + (hostId) => { + // A pane whose owner is still unresolved is not evidence of a different machine. + expect(agentResumeOriginNamesAnotherExecutionHost(TARGET_ID, hostId)).toBe(false) + } + ) +}) + +/** The SSH workspace after its host has answered, so terminal-host authority is decided and the + * sweep is allowed to act. Without the hydration mark the sweep declines for an unrelated reason + * and every assertion below would pass vacuously. */ +function seedAnsweredSshWorkspace(...records: SleepingAgentSessionRecord[]): void { + useAppStore.setState({ + repos: [{ ...TEST_REPO, id: 'repo-1', path: '/home/neil/projects', connectionId: TARGET_ID }], + worktreesByRepo: { + 'repo-1': [ + makeWorktree({ + id: WORKTREE_ID, + repoId: 'repo-1', + path: REMOTE_PATH, + hostId: `ssh:${TARGET_ID}` + }) + ] + }, + tabsByWorktree: {}, + sleepingAgentSessionsByPaneKey: Object.fromEntries( + records.map((record) => [record.paneKey, record]) + ) + }) + useAppStore.getState().markRemoteWorkspaceHydrated(TARGET_ID) +} + +describe('the resume sweep under execution-host scope', () => { + it('declines a locally captured session id rather than issuing it on the SSH host', () => { + const record = makeRecord({ connectionId: null }) + seedAnsweredSshWorkspace(record) + + expect( + resumeSleepingAgentSessionsForWorktree(WORKTREE_ID), + 'issued --resume for a local session id against the SSH host' + ).toBe(0) + expect(useAppStore.getState().tabsByWorktree[WORKTREE_ID] ?? []).toHaveLength(0) + }) + + it('preserves the declined record so the session stays resumable by hand', () => { + const record = makeRecord({ connectionId: 'a-different-target' }) + seedAnsweredSshWorkspace(record) + + resumeSleepingAgentSessionsForWorktree(WORKTREE_ID) + resumeSleepingAgentSessionsForWorktree(WORKTREE_ID) + + // Declining is recoverable only if the record survives; deleting it on a host disagreement + // would destroy the user's only handle on that transcript. + expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[record.paneKey]).toBe(record) + }) + + it('still resumes a session captured on the host that owns the workspace', () => { + const record = makeRecord({ connectionId: TARGET_ID }) + seedAnsweredSshWorkspace(record) + + expect(resumeSleepingAgentSessionsForWorktree(WORKTREE_ID)).toBe(1) + expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[record.paneKey]).toBeUndefined() + }) + + it('still resumes a legacy record that names no host at all', () => { + const record = makeRecord() + seedAnsweredSshWorkspace(record) + + expect(resumeSleepingAgentSessionsForWorktree(WORKTREE_ID)).toBe(1) + }) + + it('declines only the foreign record and resumes its native sibling', () => { + const foreign = makeRecord({ + paneKey: 'tab-1:leaf-1', + tabId: 'tab-1', + connectionId: null, + providerSession: { key: 'session_id', id: 'session-from-the-laptop' } + }) + const native = makeRecord({ paneKey: 'tab-2:leaf-1', tabId: 'tab-2', connectionId: TARGET_ID }) + seedAnsweredSshWorkspace(foreign, native) + + expect(resumeSleepingAgentSessionsForWorktree(WORKTREE_ID)).toBe(1) + const state = useAppStore.getState() + expect(state.sleepingAgentSessionsByPaneKey[foreign.paneKey]).toBe(foreign) + expect(state.sleepingAgentSessionsByPaneKey[native.paneKey]).toBeUndefined() + }) +}) diff --git a/src/renderer/src/lib/resume-sleeping-agent-session.ts b/src/renderer/src/lib/resume-sleeping-agent-session.ts index 0610eb9cf8b..50fb378888f 100644 --- a/src/renderer/src/lib/resume-sleeping-agent-session.ts +++ b/src/renderer/src/lib/resume-sleeping-agent-session.ts @@ -21,6 +21,7 @@ import { type UnhydratedHostMirror } from './host-mirrored-pane-liveness' import { parkUntilHostMirrorHandleLands } from './host-mirror-handle-gap-wait' +import { sleepingRecordNamesAnotherExecutionHost } from './sleeping-record-execution-host-scope' import { resolveWorkspaceTerminalHostAuthority } from './workspace-terminal-host-authority' import { parkUntilHostSessionMirrorHydrates } from '@/runtime/host-session-mirror-hydration' @@ -262,6 +263,14 @@ export function resumeSleepingAgentSessionsForWorktree( state.clearSleepingAgentSession(record.paneKey) continue } + // Why this is a `continue` and not a clear: the id is a valid locator on the machine that + // captured it, so the record is evidence, not garbage — deleting it on the strength of a host + // disagreement would destroy the user's only handle on that transcript. Declining costs an + // automatic wake the user can re-issue by hand; issuing `--resume` on the wrong machine is + // `No conversation found` at best and a forked transcript at worst. + if (sleepingRecordNamesAnotherExecutionHost(record, currentState)) { + continue + } const unhydratedMirror = findUnhydratedHostMirrorForPane(record, currentState) if (unhydratedMirror) { // Why: pane ownership is undecidable until the mirror answers, and every diff --git a/src/renderer/src/lib/sleeping-record-execution-host-scope.ts b/src/renderer/src/lib/sleeping-record-execution-host-scope.ts new file mode 100644 index 00000000000..8581a690f65 --- /dev/null +++ b/src/renderer/src/lib/sleeping-record-execution-host-scope.ts @@ -0,0 +1,78 @@ +import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' +import { + parseExecutionHostId, + toSshExecutionHostId, + type ExecutionHostId +} from '../../../shared/execution-host' +import { + getKnownExecutionHostIdForWorktree, + type WorktreeRuntimeOwnerState +} from './worktree-runtime-owner' + +/** + * Does this record's `--resume` locator belong to a different machine than the one the resume would + * run on? + * + * A provider session id names a transcript in one machine's agent state directory, but nothing + * else in the resume path is host-scoped: `worktreeId` is `repoId::path` with no host component + * (shared/worktree/host-qualified-identity.ts), sleeping records are `'sleepingAgentKeyed'` so the + * boot-time host-contention parking never arbitrates them and every partition's records merge into + * one map, and `launchSleepingAgentSession` resolves its launch target from the *current* catalog. + * A record captured on host A therefore reaches a launch on host B, which answers + * `No conversation found with session ID`. + * + * Deliberately fails open. It reports only a positively-known disagreement about the machine, + * because the alternative — refusing whenever the hosts cannot be compared — would strand every + * legitimate resume whose capture predates the stamp. + * + * "Fail open" names a direction for THIS decision, never a house style, and the safe direction is + * inverted a few files away. Here the destructive act is *attempting* a resume — a wrong one can + * fork a transcript, which is unrecoverable, while a refusal keeps the record and the user can + * resume by hand. So an unhydrated catalog must not be read as a host verdict. In + * `workspace-session-terminal-buffers.ts` the destructive act is the opposite: declining to capture + * loses the only scrollback copy, so an unknown repo is treated as remote. Same window, opposite + * default, both correct. A reader pattern-matching one onto the other will get this backwards. + * + * The four unknowns this fails open on: + * + * - `undefined` is "never stamped", not "local" (#9030 leaves SSH orphans unstamped). + * - `null` is "local **or** paired runtime": a `remote:@@` PTY is stamped null too + * (agent-status-connection-ownership.ts), so null cannot rule a runtime host out — only an + * `ssh:` one, which is unambiguously another machine. + * - A current host of `runtime:*` is no evidence either way, because a paired client relabels its + * host's workspaces — including that host's own SSH ones — into its runtime namespace. + * - A current host of `null` is a catalog with no row for the worktree. The routing resolver + * answers `'local'` there, which is the right default for issuing an operation and would read + * here as a positive host — so the worktree form below asks the resolver that keeps the silence. + */ +export function agentResumeOriginNamesAnotherExecutionHost( + originConnectionId: string | null | undefined, + currentExecutionHostId: ExecutionHostId | null | undefined +): boolean { + if (originConnectionId === undefined) { + return false + } + const originTargetId = originConnectionId === null ? null : originConnectionId.trim() + if (originTargetId === '') { + return false + } + const currentHost = parseExecutionHostId(currentExecutionHostId) + if (!currentHost || currentHost.kind === 'runtime') { + return false + } + if (currentHost.kind === 'ssh') { + return originTargetId === null || toSshExecutionHostId(originTargetId) !== currentHost.id + } + return originTargetId !== null +} + +/** The worktree-scoped form the activation sweep asks, resolving the host from the catalog. */ +export function sleepingRecordNamesAnotherExecutionHost( + record: SleepingAgentSessionRecord, + state: WorktreeRuntimeOwnerState +): boolean { + return agentResumeOriginNamesAnotherExecutionHost( + record.connectionId, + getKnownExecutionHostIdForWorktree(state, record.worktreeId) + ) +} diff --git a/src/renderer/src/lib/worktree-runtime-owner.test.ts b/src/renderer/src/lib/worktree-runtime-owner.test.ts index 995d7c15715..02ea0682e6b 100644 --- a/src/renderer/src/lib/worktree-runtime-owner.test.ts +++ b/src/renderer/src/lib/worktree-runtime-owner.test.ts @@ -3,6 +3,7 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { getExplicitRuntimeEnvironmentIdForWorktree, getExecutionHostIdForWorktree, + getKnownExecutionHostIdForWorktree, getRuntimeEnvironmentIdForWorktree, getRuntimeSessionMirrorEnvironmentIds, getSettingsForWorktreeRuntimeOwner, @@ -610,3 +611,39 @@ describe('active workspace host selection', () => { ) }) }) + +describe('getKnownExecutionHostIdForWorktree', () => { + const emptyCatalog: WorktreeRuntimeOwnerState = { repos: [], worktreesByRepo: {} } + + it('reports silence, not local, for a git worktree with no repo row', () => { + expect(getKnownExecutionHostIdForWorktree(emptyCatalog, 'missing-repo::wt')).toBeNull() + // The routing form keeps substituting the default in the same state. + expect(getExecutionHostIdForWorktree(emptyCatalog, 'missing-repo::wt')).toBe('local') + }) + + it('reports silence for a folder workspace with no folder-workspace row', () => { + expect(getKnownExecutionHostIdForWorktree(emptyCatalog, 'folder:missing')).toBeNull() + expect(getExecutionHostIdForWorktree(emptyCatalog, 'folder:missing')).toBe('local') + }) + + it.each([ + ['an ownerless repo row', { repos: [{ id: 'r' }] }, 'r::wt', 'local'], + ['an SSH repo row', { repos: [{ id: 'r', connectionId: 'box' }] }, 'r::wt', 'ssh:box'], + [ + 'a per-worktree host', + { worktreesByRepo: { r: [{ id: 'r::wt', repoId: 'r', hostId: 'ssh:box' }] } }, + 'r::wt', + 'ssh:box' + ], + [ + 'a folder-workspace row', + { folderWorkspaces: [{ id: 'f', projectGroupId: 'g' }] }, + 'folder:f', + 'local' + ], + ['the floating workspace', {}, FLOATING_TERMINAL_WORKTREE_ID, 'local'] + ] as const)('answers positively for %s', (_label, catalog, worktreeId, expected) => { + expect(getKnownExecutionHostIdForWorktree(catalog, worktreeId)).toBe(expected) + expect(getExecutionHostIdForWorktree(catalog, worktreeId)).toBe(expected) + }) +}) diff --git a/src/renderer/src/lib/worktree-runtime-owner.ts b/src/renderer/src/lib/worktree-runtime-owner.ts index 473b306eeaf..091cf246a9f 100644 --- a/src/renderer/src/lib/worktree-runtime-owner.ts +++ b/src/renderer/src/lib/worktree-runtime-owner.ts @@ -14,6 +14,7 @@ import { } from './worktree-runtime-owner-index' import { getSingleFocusedRuntimeEnvironmentId } from './single-runtime-legacy-owner' import { + findFolderWorkspaceOwner, getExecutionHostIdForFolderWorkspace, getExplicitRuntimeEnvironmentIdForFolderWorkspace, getRuntimeEnvironmentIdForFolderWorkspace @@ -157,10 +158,26 @@ export function getExplicitRuntimeEnvironmentIdForWorktree( return getExplicitRuntimeEnvironmentIdFromHost(getRepoExecutionHostId(repo)) } -export function getExecutionHostIdForWorktree( +function getFocusedRuntimeOrLocalExecutionHostId( + state: WorktreeRuntimeOwnerState +): ExecutionHostId { + const environmentId = getSingleFocusedRuntimeEnvironmentId(state) + return environmentId ? `runtime:${encodeURIComponent(environmentId)}` : 'local' +} + +/** + * The catalog's answer, or `null` when it has none: no row names an owner for this worktree (a git + * worktree without a repo row, a folder workspace without a folder-workspace row) and nothing more + * specific — active-workspace host, detected owner, per-worktree host — applies either. A row that + * exists and names no owner is a positive `'local'`; a row that has not landed is silence. + * {@link getExecutionHostIdForWorktree} papers over that silence with the focused-runtime-or-local + * default, which is the right answer for routing an operation and the wrong one for a caller that + * reads the host as evidence. + */ +export function getKnownExecutionHostIdForWorktree( state: WorktreeRuntimeOwnerState, worktreeId: string | null | undefined -): ExecutionHostId { +): ExecutionHostId | null { if (!worktreeId) { return 'local' } @@ -173,7 +190,11 @@ export function getExecutionHostIdForWorktree( } const workspaceScope = parseWorkspaceKey(worktreeId) if (workspaceScope?.type === 'folder') { - return getExecutionHostIdForFolderWorkspace(state, workspaceScope.folderWorkspaceId) + const hostId = getExecutionHostIdForFolderWorkspace(state, workspaceScope.folderWorkspaceId) + // Why: the folder resolver substitutes `'local'` for a missing row the same way this one does. + return hostId === 'local' && !findFolderWorkspaceOwner(state, workspaceScope.folderWorkspaceId) + ? null + : hostId } const hasDetectedOwner = hasIndexedDetectedWorktree(state.detectedWorktreesByRepo, worktreeId) if (hasDetectedOwner) { @@ -196,12 +217,24 @@ export function getExecutionHostIdForWorktree( } const repoId = worktree?.repoId ?? getRepoIdFromWorktreeId(worktreeId) const repo = findRepoRecord(state.repos, repoId) - const hasExplicitOwner = Boolean(repo?.executionHostId?.trim() || repo?.connectionId?.trim()) - if (repo && hasExplicitOwner) { + if (!repo) { + return null + } + const hasExplicitOwner = Boolean(repo.executionHostId?.trim() || repo.connectionId?.trim()) + if (hasExplicitOwner) { return getRepoExecutionHostId(repo) } - const environmentId = getSingleFocusedRuntimeEnvironmentId(state) - return environmentId ? `runtime:${encodeURIComponent(environmentId)}` : 'local' + return getFocusedRuntimeOrLocalExecutionHostId(state) +} + +export function getExecutionHostIdForWorktree( + state: WorktreeRuntimeOwnerState, + worktreeId: string | null | undefined +): ExecutionHostId { + return ( + getKnownExecutionHostIdForWorktree(state, worktreeId) ?? + getFocusedRuntimeOrLocalExecutionHostId(state) + ) } export function getSettingsForWorktreeRuntimeOwner( diff --git a/tests/e2e/ssh-stale-resume-execution-host-scope.spec.ts b/tests/e2e/ssh-stale-resume-execution-host-scope.spec.ts new file mode 100644 index 00000000000..03e72adfd98 --- /dev/null +++ b/tests/e2e/ssh-stale-resume-execution-host-scope.spec.ts @@ -0,0 +1,306 @@ +/** + * A provider session id names a transcript in ONE machine's agent state directory. Orca issued one + * against the wrong machine and the agent answered + * `No conversation found with session ID: ` in the user's remote terminal. + * + * Nothing in the resume path was host-scoped. `worktreeId` is `repoId::path` with no host component, + * sleeping records merge across every host partition at boot without retaining which one they came + * from, and the launch path resolves its target from the *current* catalog — so a record captured on + * host A reaches a `--resume` executed on host B. + * + * This lane proves it at the only altitude that settles the question: the argv that actually lands + * on the remote machine. Both tests restart the app across a relay kill (the shape of an Orca + * update, which is what the user did) and read the stub agent's argv ledger out of the container. + * + * - foreign stamp → the ledger must hold no `--resume`, and the record must survive so the user + * can still resume by hand. It must still hold Orca's ordinary `--version` + * probe, or the lane would pass on an app that never reached the host at all. + * - matching stamp → the ledger must contain `--resume `. + * + * The second is not a nicety. Without it the first passes on any app that resumes nothing at all, + * which is exactly the failure mode a refuse-everything gate would ship. + */ +import type { ElectronApplication, TestInfo } from '@stablyai/playwright-test' +import { test, expect } from './helpers/orca-app' +import { createRestartSession } from './helpers/orca-restart' +import { connectDockerSshRelayTarget } from './helpers/docker-ssh-relay-connection' +import { killDockerSshRelayDaemon } from './helpers/docker-ssh-relay-faults' +import { + cleanupDockerSshRelayTarget, + execDockerSshRelayTargetCommand, + startDockerSshRelayTarget, + writeDockerSshRelayTargetFile, + type DockerSshRelayTarget +} from './helpers/docker-ssh-relay-target' +import { waitForActiveWorktree, waitForSessionReady } from './helpers/store' +import { + waitForActivePaneHookDescriptor, + waitForActivePanePtyId, + waitForActiveTerminalManager +} from './helpers/terminal' + +const RUN_DOCKER_SSH = process.env.ORCA_E2E_SSH_DOCKER === '1' + +const SESSION_ID = 'e2e-stale-resume-87987465' +const ARGV_LEDGER = '/tmp/orca-e2e-claude-argv.log' +/** Stands in for a record the user carried over from another machine: its transcript is not on this + * host under this id. Any value that is not the connected target's works. */ +const FOREIGN_CONNECTION_ID = 'orca-e2e-some-other-host' +/** Resolve the stamp to the connected target's own id, which is only minted during connect. */ +const STAMP_OWNING_HOST = Symbol('stamp-owning-host') +/** How long a `--resume` gets to reach the host once the relaunched pane holds its PTY. The resume + * is typed into that very shell, so anything the gate let through lands well inside this. */ +const RESUME_GRACE_MS = 20_000 + +test.use({ seedTestRepo: false }) + +/** A `claude` that records the argv it was invoked with and then holds the PTY open the way the real + * binary does. The ledger outlives the pane, and is appended to rather than truncated so a second + * invocation is visible as a second line. */ +function installRemoteClaudeArgvLedger(target: DockerSshRelayTarget): void { + writeDockerSshRelayTargetFile( + target, + '/usr/local/bin/claude', + [ + '#!/bin/sh', + `printf 'ARGV [%s] pid=%s ppid=%s %s\\n' "$(date +%s)" "$$" "$PPID" "$*" >> ${ARGV_LEDGER}`, + 'exec cat', + '' + ].join('\n') + ) + execDockerSshRelayTargetCommand(target, 'chmod 755 /usr/local/bin/claude') +} + +function readRemoteArgvLedger(target: DockerSshRelayTarget): string { + return execDockerSshRelayTargetCommand(target, `cat ${ARGV_LEDGER} 2>/dev/null || true`).trim() +} + +/** The lines appended since `baseline`. The ledger is append-only and the first launch already wrote + * its own `--version` probe to it, so "non-empty" says nothing about the relaunch — only the tail + * beyond what was there at quit does. Reading the whole ledger here is exactly the race that let the + * control case read two `--version` lines and give up before the resume was typed. */ +function ledgerLinesSince(ledger: string, baseline: string): string { + return ledger.startsWith(baseline) ? ledger.slice(baseline.length).trim() : ledger +} + +/** Poll the relaunch's ledger lines until `until` holds or the budget runs out. Returns them either + * way: the negative case asserts on what did NOT arrive, so this must not throw. */ +async function settleRemoteArgvLedger( + target: DockerSshRelayTarget, + baseline: string, + budgetMs: number, + until: (fresh: string) => boolean +): Promise { + const deadline = Date.now() + budgetMs + for (;;) { + const fresh = ledgerLinesSince(readRemoteArgvLedger(target), baseline) + if (until(fresh) || Date.now() >= deadline) { + return fresh + } + await new Promise((resolve) => setTimeout(resolve, 2_000)) + } +} + +/** + * One full incident replay: capture a sleeping agent record on the SSH worktree stamped with + * `stamp`, quit, kill the relay so no PTY can be reclaimed (without that the pane's live PTY + * suppresses the resume and the test proves nothing), relaunch, and report what reached the remote. + */ +async function resumeAcrossRestart( + testInfo: TestInfo, + target: DockerSshRelayTarget, + stamp: string | typeof STAMP_OWNING_HOST, + resumeBudgetMs: number +): Promise<{ + ledger: string + recordSurvived: boolean + diagnostics: { + recordStamp: string + entryStamp: string + ledgerBeforeQuit: string + ledgerAfterQuit: string + } +}> { + const restart = createRestartSession(testInfo) + let firstApp: ElectronApplication | null = null + let secondApp: ElectronApplication | null = null + try { + const firstLaunch = await restart.launch() + firstApp = firstLaunch.app + await waitForSessionReady(firstLaunch.page) + const remote = await connectDockerSshRelayTarget(firstLaunch.page, target) + await expect + .poll(() => waitForActiveWorktree(firstLaunch.page), { timeout: 60_000 }) + .toBe(remote.worktreeId) + await waitForActiveTerminalManager(firstLaunch.page, 60_000) + const descriptor = await waitForActivePaneHookDescriptor(firstLaunch.page, 60_000) + + // Why seeded rather than driven by a real agent: a real `claude` run needs an install and auth + // in the container. This is the same store entry the hook server writes, so the capture, + // persistence and resume paths under test are the production ones. + await firstLaunch.page.evaluate( + ({ paneKey, worktreeId, providerSessionId, connectionId }) => { + window.__store?.getState().setAgentStatus( + paneKey, + { state: 'working', prompt: 'finish the task', agentType: 'claude' }, + 'Claude', + undefined, + { worktreeId, connectionId }, + { + providerSession: { key: 'session_id', id: providerSessionId }, + launchConfig: { agentCommand: 'claude', agentArgs: '', agentEnv: {} } + } + ) + }, + { + paneKey: descriptor.paneKey, + worktreeId: remote.worktreeId, + providerSessionId: SESSION_ID, + connectionId: stamp === STAMP_OWNING_HOST ? remote.targetId : stamp + } + ) + + await firstLaunch.page.evaluate(() => window.dispatchEvent(new Event('beforeunload'))) + await expect + .poll( + () => + firstLaunch.page.evaluate( + async ({ targetId, sessionId }) => { + // The SSH worktree's rows live in the `ssh:` partition, globals in `local`. + const [local, host] = await Promise.all([ + window.api.session.get(), + window.api.session.get(`ssh:${targetId}`) + ]) + return [ + ...Object.values(local.sleepingAgentSessionsByPaneKey ?? {}), + ...Object.values(host.sleepingAgentSessionsByPaneKey ?? {}) + ].some((record) => record.providerSession.id === sessionId) + }, + { targetId: remote.targetId, sessionId: SESSION_ID } + ), + { timeout: 30_000, message: 'the sleeping agent record was never persisted before quit' } + ) + .toBe(true) + + const ledgerBeforeQuit = readRemoteArgvLedger(target) + + await restart.close(firstApp) + firstApp = null + // The shape of an Orca update: the relay and every PTY under it are gone, so nothing is + // reclaimable and the sleeping record is the only way the agent comes back. + killDockerSshRelayDaemon(target) + const ledgerAfterQuit = readRemoteArgvLedger(target) + + const secondLaunch = await restart.launch() + secondApp = secondLaunch.app + await waitForSessionReady(secondLaunch.page, 60_000) + await expect + .poll(() => waitForActiveWorktree(secondLaunch.page), { timeout: 90_000 }) + .toBe(remote.worktreeId) + await waitForActiveTerminalManager(secondLaunch.page, 90_000) + // The cold-restore decision is made before the replacement PTY is spawned, so a bound PTY + // means the gate has already ruled on this record — after this, waiting is only for the + // typed command to travel. + await waitForActivePanePtyId(secondLaunch.page, 90_000) + // Orca's per-launch `claude --version` probe proves the relaunch reached the host at all; the + // negative case is vacuous without it. + await settleRemoteArgvLedger(target, ledgerAfterQuit, 90_000, (fresh) => + fresh.includes('--version') + ) + const ledger = await settleRemoteArgvLedger(target, ledgerAfterQuit, resumeBudgetMs, (fresh) => + fresh.includes('--resume') + ) + // Why this is reported rather than merely asserted: the two host stamps are what the gate reads, + // so a failure that does not name them cannot be told apart from the gate simply not running. + const diagnostics = await secondLaunch.page.evaluate((sessionId) => { + const state = window.__store?.getState() + const record = Object.values(state?.sleepingAgentSessionsByPaneKey ?? {}).find( + (candidate) => candidate.providerSession.id === sessionId + ) + const entry = Object.values(state?.agentStatusByPaneKey ?? {}).find( + (candidate) => candidate.providerSession?.id === sessionId + ) + return { + recordStamp: record ? String(record.connectionId) : 'no-record', + entryStamp: entry ? String(entry.connectionId) : 'no-entry' + } + }, SESSION_ID) + return { + ledger, + recordSurvived: diagnostics.recordStamp !== 'no-record', + diagnostics: { ...diagnostics, ledgerBeforeQuit, ledgerAfterQuit } + } + } finally { + if (secondApp) { + await restart.close(secondApp) + } + if (firstApp) { + await restart.close(firstApp) + } + await restart.dispose() + } +} + +test.describe('SSH sleeping-agent resume execution-host scope', () => { + test.skip(!RUN_DOCKER_SSH, 'Set ORCA_E2E_SSH_DOCKER=1 to run Docker-backed SSH tests.') + test.skip(process.platform === 'win32', 'Docker SSH tests use POSIX ssh tooling.') + test.describe.configure({ mode: 'serial' }) + + test("does not issue another host's session id against the SSH host", async (// oxlint-disable-next-line no-empty-pattern -- This restart test owns both Electron launches. + {}, testInfo) => { + test.setTimeout(600_000) + let target: DockerSshRelayTarget | null = null + try { + target = startDockerSshRelayTarget(testInfo) + installRemoteClaudeArgvLedger(target) + + const result = await resumeAcrossRestart( + testInfo, + target, + FOREIGN_CONNECTION_ID, + RESUME_GRACE_MS + ) + + // Why not an empty ledger: Orca legitimately probes `claude --version` on the remote to + // detect installed agents, once per launch. That is not a resume. The defect is `--resume` + // carrying an id this machine never wrote, so that is what must be absent. `result.ledger` + // is only what the relaunch appended, so the first launch's probe cannot satisfy this. + expect( + result.ledger, + `Orca ran the agent on the SSH host with a session id captured on another machine.\nrecord stamp: ${result.diagnostics.recordStamp}\nlive entry stamp: ${result.diagnostics.entryStamp}\nledger before quit: ${JSON.stringify(result.diagnostics.ledgerBeforeQuit)}\nledger after quit+relay kill: ${JSON.stringify(result.diagnostics.ledgerAfterQuit)}` + ).not.toContain('--resume') + expect(result.ledger).not.toContain(SESSION_ID) + // The relaunch's lines must not be empty either, or this proves only that the agent never + // ran at all. + expect( + result.ledger, + 'the stub agent was never invoked by the relaunch, so the lane proves nothing' + ).toContain('--version') + // Declining is only recoverable if the record survives; deleting it on a host disagreement + // would destroy the user's only handle on that transcript. + expect(result.recordSurvived, 'the declined record was discarded, not preserved').toBe(true) + } finally { + cleanupDockerSshRelayTarget(target) + } + }) + + test('still resumes a session captured on the SSH host that owns the workspace', async (// oxlint-disable-next-line no-empty-pattern -- This restart test owns both Electron launches. + {}, testInfo) => { + test.setTimeout(600_000) + let target: DockerSshRelayTarget | null = null + try { + target = startDockerSshRelayTarget(testInfo) + installRemoteClaudeArgvLedger(target) + + // The control for the test above: the same machinery, one field different, and the resume + // must still land on the remote. + const result = await resumeAcrossRestart(testInfo, target, STAMP_OWNING_HOST, 90_000) + + expect(result.ledger, 'the legitimate resume never reached the SSH host').toContain( + `--resume ${SESSION_ID}` + ) + } finally { + cleanupDockerSshRelayTarget(target) + } + }) +}) From b7d694ff7ed85d6ab48df24ed0df9b1dd58091d2 Mon Sep 17 00:00:00 2001 From: Vincent <47273853+Tkotm76@users.noreply.github.com> Date: Fri, 18 Sep 2026 07:31:31 +0200 Subject: [PATCH 56/59] feat(composer): choose a base ref in the New Workspace composer (#17250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(repo): share the create-from picker outside automations Move CreateFromPicker and its test from components/automations to components/repo, next to the repo-scoped shared UI that already lives there (RepoCombobox, RepoBadgeLabel, repo-icon). The New Workspace composer will consume this picker instead of growing a second base-ref combobox. Pure move: no behavior change. The translate() keys are call-site literals, so no locale catalog is affected. Co-Authored-By: Claude Opus 5 (1M context) * fix(composer): separate the branch that names a workspace from its base baseBranch carried two meanings at once. It is the ref a worktree is created from, and it is also what buildWorkspaceSourceSelection turns into the name field's branch pill whenever no work item is linked. Any second control that set a base therefore took the name field over: the pill replaced the text input, hiding whatever the user had typed. The name survived in state, and Advanced still exposed it, but the main field silently stopped showing it. Add baseBranchNamesWorkspace, true only when a branch was picked to name the workspace. The pill reads that flag; creation keeps reading baseBranch. Two call sites set it, because those are the only paths that make baseBranch defined with nothing linked — and an undefined base yields no pill anyway. Co-Authored-By: Claude Opus 5 (1M context) * feat(composer): let the New Workspace composer pick its base ref The name field's tabs pick how a workspace is named; the base ref is a separate decision the composer never exposed. Naming a workspace from a Jira, Linear, GitHub or GitLab issue therefore pinned the project's default base with no way to start from a release or a long-lived feature branch. Nothing below the UI was missing. baseBranch already crosses IPC next to linkedWorkItem and wins over every default in main, and the composer already computed handleBaseBranchChange and startFromResetHint — the card simply never declared those props, so its {...props} spread dropped them. Declare them and render the shared create-from picker under the name field. ComposerBaseRefPicker owns its own store reads, the way the sibling ComposerParentWorktreePicker already does, so the name section stays presentational and nothing subscribes to the worktree list while the picker is hidden. The picker is offered for a plain typed name and for issue-shaped sources. It is hidden where a base already exists: PR/MR sources pin the pull request's own head, a branch pick IS the base — and offering one there would silently turn a checkout of that branch into a new branch off something else, since picking a base clears reuse — and folder workspaces have no branches. It always opens on the project default: no sticky base. Co-Authored-By: Claude Opus 5 (1M context) * chore(repo): drop a stale react-doctor suppression on the create-from picker no-adjust-state-on-prop-change no longer fires on this file: removing the directive and running the react-doctor pass over the directory — where the JS plugin actually loads — reports nothing, at the new path and at the old one on main alike. The suppression was already dead; the rename only put the file in the changed set, where the quality gate reports unused directives. Co-Authored-By: Claude Opus 5 (1M context) * feat(repo): list branches as soon as the create-from picker opens The picker only searched once two characters were typed, so opening it showed just the project default and whatever branches already had a worktree. The composer's Branch tab lists on an empty query through the same runtime helper; match it, and the picker offers the repo's branches straight away. Search stays debounced at 200ms and capped at 30 results, and it still runs on the repo's own execution host, so a remote repo lists its own branches. The Automations picker shares this component and gains the same listing. Co-Authored-By: Claude Opus 5 (1M context) * fix(composer): carry the base-ref naming intent through a saved draft `baseBranchNamesWorkspace` lived only in component state, so restoring a persisted draft always reset it to true. A base ref chosen in the picker came back as a name-field source pill, hiding the name the user had typed — the exact regression the flag exists to prevent, reappearing across a draft round trip. Persist it next to `baseBranch` and restore it through `resolveDraftBaseBranchNamesWorkspace`. A draft written before the flag existed records no intent and restores as a branch pick, which is the behavior it had when it was saved. Co-Authored-By: Claude Opus 5 (1M context) * fix(composer): preserve independent base and branch name choices * fix(composer): pass naming-intent through the create-more reset test IssueSourceActions now requires baseBranchNamesWorkspace. The create-more reset fixture is a source-owned base, so the flag stays true and the next create still clears it. --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Neil --- ...wWorkspaceComposerCard.start-from.test.tsx | 229 ++++++++++++++++++ .../automations/AutomationWorkspaceField.tsx | 2 +- .../new-workspace/ComposerBaseRefPicker.tsx | 42 ++++ .../NewWorkspaceComposerNameSection.tsx | 25 +- .../base-ref-picker-visibility.test.ts | 44 ++++ .../base-ref-picker-visibility.ts | 25 ++ .../new-workspace-composer-card-props.ts | 3 + .../CreateFromPicker.test.tsx | 36 ++- .../CreateFromPicker.tsx | 5 +- .../branch-start-point-actions.test.ts | 95 ++++++++ .../branch-start-point-actions.ts | 24 +- .../composer-state/composer-external-sync.ts | 1 + .../composer-name-source-selection.test.ts | 193 +++++++++++++++ .../composer-state/composer-source-state.ts | 6 + .../composer-state/composer-target-state.ts | 1 + .../composer-state/draft-target-sync.test.ts | 20 ++ .../hooks/composer-state/draft-target-sync.ts | 5 +- .../github-provider-selection.ts | 7 +- .../github-submit-resolution.ts | 5 + .../hooks/composer-state/identity-model.ts | 2 + .../composer-state/issue-source-actions.ts | 18 +- .../multiple-create-reset.test.ts | 2 + .../work-item-source-actions.ts | 4 + .../workspace-identity-state.ts | 23 ++ .../store/slices/ui/ui-slice-contract-core.ts | 3 + 25 files changed, 805 insertions(+), 15 deletions(-) create mode 100644 src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx create mode 100644 src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx create mode 100644 src/renderer/src/components/new-workspace/base-ref-picker-visibility.test.ts create mode 100644 src/renderer/src/components/new-workspace/base-ref-picker-visibility.ts rename src/renderer/src/components/{automations => repo}/CreateFromPicker.test.tsx (74%) rename src/renderer/src/components/{automations => repo}/CreateFromPicker.tsx (97%) create mode 100644 src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts create mode 100644 src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx new file mode 100644 index 00000000000..5b186fc9378 --- /dev/null +++ b/src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx @@ -0,0 +1,229 @@ +// @vitest-environment happy-dom + +import React, { act } from 'react' +import { createRoot } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import NewWorkspaceComposerCard from './NewWorkspaceComposerCard' + +vi.mock('@/store', () => ({ + useAppStore: Object.assign( + (selector: (state: unknown) => unknown) => + selector({ + closeModal: vi.fn(), + openModal: vi.fn(), + openSettingsPage: vi.fn(), + openSettingsTarget: vi.fn(), + setRuntimeEnvironmentStatus: vi.fn(), + activeModal: 'new-workspace-composer', + settings: { defaultTuiAgent: null, disabledTuiAgents: [] }, + updateSettings: vi.fn(), + projects: [], + repos: [], + worktreesByRepo: {} + }), + { getState: () => ({}) } + ) +})) + +vi.mock('@/components/contextual-tours/use-contextual-tour', () => ({ + useContextualTour: vi.fn() +})) + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, + TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}, + TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children} +})) + +vi.mock('@/components/agent/AgentCombobox', () => ({ + default: () => +})) + +vi.mock('@/components/sidebar/AddRemoteHostDialog', () => ({ + AddRemoteHostDialog: () => null +})) + +vi.mock('@/components/new-workspace/SmartWorkspaceNameField', () => ({ + default: () => +})) + +vi.mock('@/components/new-workspace/ProjectCombobox', () => ({ + default: () =>
+})) + +// Why: the picker owns its own test; here it only has to report its value and emit picks. +vi.mock('@/components/repo/CreateFromPicker', () => ({ + CreateFromPicker: ({ + value, + onValueChange + }: { + value: string + onValueChange: (next: string) => void + }) => ( +
+ + +
+ ) +})) + +function renderCard( + overrides: Partial> = {} +): HTMLDivElement { + const container = document.createElement('div') + document.body.appendChild(container) + act(() => { + createRoot(container).render( + {}} + eligibleRepos={[]} + repoId="repo-a" + selectedRepoIsGit + onRepoChange={() => {}} + onProjectChange={() => {}} + primaryActionLabel="Create workspace" + name="" + onNameValueChange={() => {}} + branchNameOverride={undefined} + onBranchNameOverrideChange={() => {}} + onSmartGitHubItemSelect={() => {}} + onSmartGitLabItemSelect={() => {}} + onSmartBranchSelect={() => {}} + onSmartLinearIssueSelect={() => {}} + smartNameSelection={{ kind: 'jira', label: 'ERP-1491' }} + onClearSmartNameSelection={() => {}} + canReuseSelectedBranch={false} + reuseSelectedBranch={false} + onReuseSelectedBranchChange={() => {}} + forkPushWarning={null} + detectedAgentIds={null} + onOpenAgentSettings={() => {}} + advancedOpen={false} + onToggleAdvanced={() => {}} + parentWorktreeId={null} + onParentWorktreeIdChange={() => {}} + createDisabled={false} + projectError={null} + creating={false} + onCreate={() => {}} + note="" + onNoteChange={() => {}} + setupConfig={null} + requiresExplicitSetupChoice={false} + setupDecision={null} + onSetupDecisionChange={() => {}} + setupAgentStartupPolicy="start-immediately" + onSetupAgentStartupPolicyChange={() => {}} + shouldWaitForSetupCheck={false} + resolvedSetupDecision={null} + createError={null} + selectedRepoConnectionId={null} + selectedRepoSshStatus={null} + selectedRepoRequiresConnection={false} + selectedRepoConnectInProgress={false} + onConnectSelectedRepo={async () => {}} + canUseSparseCheckout={false} + sparsePresets={[]} + sparseSelectedPresetId={null} + onSparseSelectPreset={() => {}} + branchesEnabled + setupControlsEnabled={false} + sparseControlsEnabled={false} + baseBranch={undefined} + onBaseBranchChange={() => {}} + startFromResetHint={null} + {...overrides} + /> + ) + }) + return container +} + +function clickButton(container: HTMLDivElement, label: string): void { + const button = [...container.querySelectorAll('button')].find( + (candidate) => candidate.textContent === label + ) + act(() => button?.click()) +} + +describe('NewWorkspaceComposerCard start from', () => { + let container: HTMLDivElement | null = null + + afterEach(() => { + container?.remove() + container = null + }) + + it('offers a base ref while a Jira issue names the workspace', () => { + container = renderCard() + + expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeTruthy() + }) + + it('reports the picked ref to the composer', () => { + const picks: (string | undefined)[] = [] + container = renderCard({ onBaseBranchChange: (next) => picks.push(next) }) + + clickButton(container, 'Pick release') + + expect(picks).toEqual(['release/1.2']) + }) + + it('reports the project default as no base at all', () => { + const picks: (string | undefined)[] = [] + container = renderCard({ + baseBranch: 'release/1.2', + onBaseBranchChange: (next) => picks.push(next) + }) + + clickButton(container, 'Pick project default') + + expect(picks).toEqual([undefined]) + }) + + // Why: picking a base clears reuse, so offering one here would silently turn a checkout of + // the picked branch into a new branch off something else. + it('omits the base ref for a branch source, which already is the base', () => { + container = renderCard({ + smartNameSelection: { kind: 'branch', label: 'feature/export-v2' }, + baseBranch: 'feature/export-v2' + }) + + expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeNull() + }) + + it('offers the base ref while a plain typed name owns the field', () => { + container = renderCard({ smartNameSelection: null, name: 'my-own-name' }) + + expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeTruthy() + }) + + it.each([ + ['github-pr', { kind: 'github-pr' as const, label: '#42 Fix' }], + ['gitlab-mr', { kind: 'gitlab-mr' as const, label: '!42 Fix' }] + ])( + 'omits the base ref for a %s source that carries its own base', + (_label, smartNameSelection) => { + container = renderCard({ smartNameSelection }) + + expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeNull() + } + ) + + it('omits the base ref when branches are disabled', () => { + container = renderCard({ branchesEnabled: false }) + + expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeNull() + }) + + it('surfaces the reset hint left by a project switch', () => { + container = renderCard({ startFromResetHint: 'was origin/main' }) + + expect(container.textContent).toContain('was origin/main') + }) +}) diff --git a/src/renderer/src/components/automations/AutomationWorkspaceField.tsx b/src/renderer/src/components/automations/AutomationWorkspaceField.tsx index 80e6feeda32..9bbcf37cb9a 100644 --- a/src/renderer/src/components/automations/AutomationWorkspaceField.tsx +++ b/src/renderer/src/components/automations/AutomationWorkspaceField.tsx @@ -1,12 +1,12 @@ import { Info } from 'lucide-react' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { CreateFromPicker } from '@/components/repo/CreateFromPicker' import { translate } from '@/i18n/i18n' import type { AutomationWorkspaceMode } from '../../../../shared/automations-types' import type { Repo } from '../../../../shared/repo-types' import type { Worktree } from '../../../../shared/worktree/types' import { AUTOMATION_EDITOR_SECTION_LABEL_CLASS, Field } from './automation-page-parts' -import { CreateFromPicker } from './CreateFromPicker' import { WorkspaceCombobox } from './WorkspaceCombobox' import type { AutomationDraft } from './AutomationEditorDialog' diff --git a/src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx b/src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx new file mode 100644 index 00000000000..e97f980ee87 --- /dev/null +++ b/src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { CreateFromPicker } from '@/components/repo/CreateFromPicker' +import { useRepoMap, useWorktreesForRepo } from '@/store/selectors' + +type ComposerBaseRefPickerProps = { + repoId: string + baseBranch: string | undefined + onBaseBranchChange: (value: string | undefined) => void + resetHint: string | null | undefined +} + +/** + * Base ref control for the New Workspace composer. + * + * Owns its own store reads so the name section stays presentational and the + * worktree subscription only exists while the picker is actually on screen. + */ +export function ComposerBaseRefPicker({ + repoId, + baseBranch, + onBaseBranchChange, + resetHint +}: ComposerBaseRefPickerProps): React.JSX.Element { + const repoMap = useRepoMap() + const repoWorktrees = useWorktreesForRepo(repoId) + return ( +
+ onBaseBranchChange(nextBaseBranch || undefined)} + /> + {resetHint ?

{resetHint}

: null} +
+ ) +} + +export default ComposerBaseRefPicker diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx index 225e0f790a5..f05ef9d06a1 100644 --- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx +++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx @@ -3,6 +3,8 @@ import { AlertTriangle, Check } from 'lucide-react' import SmartWorkspaceNameField from '@/components/new-workspace/SmartWorkspaceNameField' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' +import { shouldShowComposerBaseRefPicker } from './base-ref-picker-visibility' +import { ComposerBaseRefPicker } from './ComposerBaseRefPicker' import type { NewWorkspaceComposerCardProps } from './new-workspace-composer-card-props' type NewWorkspaceComposerNameSectionProps = Pick< @@ -35,6 +37,9 @@ type NewWorkspaceComposerNameSectionProps = Pick< | 'canReuseSelectedBranch' | 'reuseSelectedBranch' | 'onReuseSelectedBranchChange' + | 'baseBranch' + | 'onBaseBranchChange' + | 'startFromResetHint' > & { onNamePlainEnter: () => void } @@ -68,8 +73,18 @@ export function NewWorkspaceComposerNameSection({ forkPushWarning, canReuseSelectedBranch, reuseSelectedBranch, - onReuseSelectedBranchChange + onReuseSelectedBranchChange, + baseBranch, + onBaseBranchChange, + startFromResetHint }: NewWorkspaceComposerNameSectionProps): React.JSX.Element { + const showBaseRefPicker = + Boolean(onBaseBranchChange) && + shouldShowComposerBaseRefPicker({ + selectedRepoIsGit, + branchesEnabled, + smartNameSelectionKind: smartNameSelection?.kind ?? null + }) return (