diff --git a/.gitattributes b/.gitattributes index 1b447a9189e..145c06043bd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -41,3 +41,10 @@ # Generated method->params catalog: compared byte-for-byte by # verify:rpc-params-catalog, so a CRLF checkout would fail the gate. /src/shared/rpc-contract/rpc-params-catalog.generated.ts linguist-generated=true text eol=lf +# Mobile web bundle source. Every text byte here is hashed into an asset digest and +# from there into buildId, so a CRLF checkout produces a different bundle id for the +# same commit (91af2897 vs 9d78435e). The PNG is -text because it must not be touched. +/src/mobile-web/index.html text eol=lf +/src/mobile-web/src/*.ts text eol=lf +/src/mobile-web/src/*.css text eol=lf +/src/mobile-web/src/*.png -text diff --git a/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml b/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml index fa2d55d3d40..617c852d304 100644 --- a/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml +++ b/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml @@ -57,6 +57,9 @@ jobs: GATE_OVERRIDE_REASON: ${{ inputs.gate-override-reason }} GATE_OVERRIDE_CONFIRMATION: ${{ inputs.gate-override-confirmation }} OUTPUT_DIRECTORY: ${{ github.workspace }}/relay-monitor-evidence + # ~800 controls over 2 min is ~7 re-dials/s per cell, well under the director's + # 5 x 80 in-flight assign cap. A cell on an older image ignores it and drains at once. + DRAIN_PACE_WINDOW_MS: '120000' steps: - name: Require exact reusable-workflow configuration working-directory: . @@ -84,12 +87,6 @@ jobs: "SKIP_RELAY_MONITOR_GATE ${TARGET_IMAGE_DIGEST}" [[ "${GATE_OVERRIDE_REASON}" =~ ^[[:print:]]{12,500}$ ]] fi - if test "${DEPLOY_MODE}" = verify; then - EFFECTIVE_SELECTOR_GENERATION="${EXPECTED_SELECTOR_GENERATION}" - else - EFFECTIVE_SELECTOR_GENERATION="$((EXPECTED_SELECTOR_GENERATION + (2 * WAVE_INDEX)))" - fi - echo "EFFECTIVE_SELECTOR_GENERATION=${EFFECTIVE_SELECTOR_GENERATION}" >> "${GITHUB_ENV}" if test "${DEPLOY_MODE}" != verify && test "${GITHUB_RUN_ATTEMPT}" != 1; then echo "mutations are single-dispatch: re-runs replay aged evidence," >&2 echo "so recover each remaining cell with its own fresh monitor" >&2 @@ -121,6 +118,26 @@ jobs: - uses: hashicorp/setup-terraform@v3 with: { terraform_wrapper: false } + # One approved-cell table, in the wave validator the dispatch gate already uses, so + # a cell's class and its wave's selector delta cannot drift apart between the two. + - name: Resolve this cell's admission class and wave selector delta + run: | + CELL_CLASS="$(node dev/scripts/relay-production-same-cap-wave.mjs cell-class \ + --cell-id "${TARGET_CELL_ID}")" + ENTRY_ADMISSION="$(jq -er '.entryAdmission' <<< "${CELL_CLASS}")" + SELECTOR_WAVE_DELTA="$(jq -er '.selectorWaveDelta' <<< "${CELL_CLASS}")" + if test "${DEPLOY_MODE}" = verify; then + EFFECTIVE_SELECTOR_GENERATION="${EXPECTED_SELECTOR_GENERATION}" + else + EFFECTIVE_SELECTOR_GENERATION="$((EXPECTED_SELECTOR_GENERATION \ + + (SELECTOR_WAVE_DELTA * WAVE_INDEX)))" + fi + { + echo "ENTRY_ADMISSION=${ENTRY_ADMISSION}" + echo "SELECTOR_WAVE_DELTA=${SELECTOR_WAVE_DELTA}" + echo "EFFECTIVE_SELECTOR_GENERATION=${EFFECTIVE_SELECTOR_GENERATION}" + } >> "${GITHUB_ENV}" + - name: Require fresh aggregate monitor evidence reference if: ${{ inputs.mode != 'verify' && inputs.gate-override-confirmation == '' }} run: | @@ -214,11 +231,13 @@ jobs: --no-monitor-state \ --expected-selector-generation "${EXPECTED_SELECTOR_GENERATION}" \ --selector-membership-file "${RUNNER_TEMP}/relay-same-cap-selector.json" \ - --wave-index "${WAVE_INDEX}" --retry-freshness + --wave-index "${WAVE_INDEX}" \ + --selector-wave-delta "${SELECTOR_WAVE_DELTA}" --retry-freshness else pnpm incident:relay-preflight -- \ --state-file "${OUTPUT_DIRECTORY}/relay-${MONITOR_RUN_ID}-dry-run.state.json" \ - --wave-index "${WAVE_INDEX}" --retry-freshness + --wave-index "${WAVE_INDEX}" \ + --selector-wave-delta "${SELECTOR_WAVE_DELTA}" --retry-freshness fi - name: Require durable rehome disabled and exact selector @@ -248,6 +267,11 @@ jobs: EXPECTED_REGION=us-central1 EXPECTED_DATABASE_POOL_MAX= ;; + c17|c18) + EXPECTED_HARD_CAP=600 + EXPECTED_REGION=us-central1 + EXPECTED_DATABASE_POOL_MAX= + ;; c27|c28|c29) EXPECTED_HARD_CAP=3000 EXPECTED_REGION=asia-east2 @@ -263,10 +287,6 @@ jobs: SOURCE_CELLS="$(terraform -chdir=infra/terraform console \ -var-file=environments/production.tfvars \ <<< 'jsonencode(var.relay_region_rehome_source_cell_ids)' | jq -er '.')" - if test "${EXPECTED_REGION}" = us-central1; then - jq -e --arg cell "${TARGET_CELL_ID}" 'index($cell) != null' \ - <<< "${SOURCE_CELLS}" >/dev/null - fi CURRENT_SHAPE="$(jq -cer --arg cell "${TARGET_CELL_ID}" '.[$cell]' <<< "${CELLS_JSON}")" test "$(jq -r '.connection_hard_cap' <<< "${CURRENT_SHAPE}")" = "${EXPECTED_HARD_CAP}" test "$(jq -r '.connection_unobserved_bound' <<< "${CURRENT_SHAPE}")" = \ @@ -288,6 +308,14 @@ jobs: DESIRED_REHOME_PROTOCOL="${TARGET_REHOME_PROTOCOL}" CURRENT_REHOME_PROTOCOL="${ROLLBACK_REHOME_PROTOCOL}" fi + # The startup template emits rehome trust lines only for a declared source cell, so + # require membership exactly when either side of this roll expects those lines. + if test "${EXPECTED_REGION}" = us-central1 && { + test "${DESIRED_REHOME_PROTOCOL}" != 0 || test "${CURRENT_REHOME_PROTOCOL}" != 0 + }; then + jq -e --arg cell "${TARGET_CELL_ID}" 'index($cell) != null' \ + <<< "${SOURCE_CELLS}" >/dev/null + fi DESIRED_IMAGE="${IMAGE_REPOSITORY}@${DESIRED_IMAGE_DIGEST}" OVERRIDE_CELLS_JSON="$(jq -ce --arg cell "${TARGET_CELL_ID}" \ --arg image "${DESIRED_IMAGE}" '.[$cell].image = $image' <<< "${CELLS_JSON}")" @@ -332,20 +360,66 @@ jobs: } CURRENT_RUNTIME="$(admin_post current-runtime \ "${CELL_ORIGIN}/v1/admin/runtime-status" '{"v":1}')" - # A rollback that failed between template apply and admission restore - # leaves the cell already on the rollback image; resume from that - # state instead of demanding the pre-rollback predecessor. + # Two different failures leave the cell on the rollback image, and the image + # alone cannot tell them apart. A rollback that failed between its template + # apply and its admission restore restarted the cell, so that cell is not + # draining and resumes. A wave that stopped after its drain and before its + # template apply never restarted anything, so its cell is still draining and + # is stranded: the drain flag only clears on a restart, so it has to be rolled. LIVE_IMAGE_DIGEST="$(jq -r '.imageDigest' <<< "${CURRENT_RUNTIME}")" + LIVE_DRAINING="$(jq -r '.draining' <<< "${CURRENT_RUNTIME}")" if test "${DEPLOY_MODE}" = rollback \ && test "${LIVE_IMAGE_DIGEST}" = "${DESIRED_IMAGE_DIGEST}"; then - ROLLBACK_RESUME=true + if test "${LIVE_DRAINING}" = true; then + ROLLBACK_STAGE=stranded + else + ROLLBACK_STAGE=resume + fi PREDECESSOR_IMAGE_DIGEST="${DESIRED_IMAGE_DIGEST}" PREDECESSOR_REHOME_PROTOCOL="${DESIRED_REHOME_PROTOCOL}" else - ROLLBACK_RESUME=false + ROLLBACK_STAGE=roll PREDECESSOR_IMAGE_DIGEST="${CURRENT_IMAGE_DIGEST}" PREDECESSOR_REHOME_PROTOCOL="${CURRENT_REHOME_PROTOCOL}" fi + if test "${ROLLBACK_STAGE}" = resume; then + ROLLBACK_RESUME=true + else + ROLLBACK_RESUME=false + fi + # A stranded cell's template still carries the image the cell is serving, so that + # is the predecessor its plan is reviewed against. A template already moved on to + # the target is refused here rather than rolled backwards under a stale review. + if test "${ROLLBACK_STAGE}" = stranded; then + PLAN_ROLLBACK_IMAGE="${DESIRED_IMAGE}" + else + PLAN_ROLLBACK_IMAGE="${IMAGE_REPOSITORY}@${CURRENT_IMAGE_DIGEST}" + fi + # Rollback is the documented recovery from a failed canary, which + # leaves the cell migration-only (and possibly still marked + # draining); apply and verify still require the cell pristine in the + # class it is declared to serve in. + if test "${DEPLOY_MODE}" = rollback; then + PRECHECK_ADMISSION=general-or-migration-only + else + PRECHECK_ADMISSION="${ENTRY_ADMISSION}" + fi + # Draining sheds connections, and a migration-only cell holds none, so the flag + # carries no precondition there. It also outlives a failed wave, because the drain + # that set it is followed by no restart, which is the state a failed canary leaves. + if test "${DEPLOY_MODE}" = rollback \ + || test "${ENTRY_ADMISSION}" = migration-only; then + PRECHECK_DRAINING=either + else + PRECHECK_DRAINING=forbidden + fi + # A resumed rollback already restarted, so its cell has to come back not draining; + # that is what separates it from a wave that stopped before its template apply. + if test "${PRECHECK_DRAINING}" = either && test "${ROLLBACK_RESUME}" != true; then + PREDECESSOR_DRAINING_OK=true + else + PREDECESSOR_DRAINING_OK=false + fi RESTORED_MIGRATION_CELLS="$(jq -rn \ --arg value "${EXPECTED_MIGRATION_ONLY_CELLS/none/}" \ --arg target "${TARGET_CELL_ID}" \ @@ -366,8 +440,19 @@ jobs: '$value | split(",") | map(select(length > 0 and . != $target)) | unique | join(",")')" test -n "${ISOLATED_MIGRATION_CELLS}" || ISOLATED_MIGRATION_CELLS=none test -n "${ISOLATED_GENERAL_CELLS}" || ISOLATED_GENERAL_CELLS=none + # A migration-only cell is already isolated and is handed back isolated, so both + # halves of its wave see exactly the membership it entered with. + if test "${ENTRY_ADMISSION}" = migration-only; then + RESTORED_MIGRATION_CELLS="${ISOLATED_MIGRATION_CELLS}" + RESTORED_GENERAL_CELLS="${ISOLATED_GENERAL_CELLS}" + fi { echo "ROLLBACK_RESUME=${ROLLBACK_RESUME}" + echo "ROLLBACK_STAGE=${ROLLBACK_STAGE}" + echo "PLAN_ROLLBACK_IMAGE=${PLAN_ROLLBACK_IMAGE}" + # The drain wait and the plan review both read the image this cell actually + # serves, which is the rollback image on a stranded cell and not the current one. + echo "PREDECESSOR_IMAGE_DIGEST=${PREDECESSOR_IMAGE_DIGEST}" # The failsafe consumes these; deriving them here keeps them # defined for a failure in any later step. echo "ISOLATED_MIGRATION_CELLS=${ISOLATED_MIGRATION_CELLS}" @@ -386,8 +471,7 @@ jobs: --argjson hardCap "${EXPECTED_HARD_CAP}" \ --argjson unobservedBound "${EXPECTED_UNOBSERVED_BOUND}" \ --argjson protocol "${PREDECESSOR_REHOME_PROTOCOL}" \ - --argjson drainingOk "$(test "${DEPLOY_MODE}" = rollback \ - && test "${ROLLBACK_RESUME}" != true && echo true || echo false)" \ + --argjson drainingOk "${PREDECESSOR_DRAINING_OK}" \ '.role == "cell" and .cellId == $cell and .cellUrl == $origin and (.region == $region or ($region == "us-central1" and $protocol == 0 and .region == null)) and @@ -403,8 +487,7 @@ jobs: --argjson hardCap "${EXPECTED_HARD_CAP}" \ --argjson unobservedBound "${EXPECTED_UNOBSERVED_BOUND}" \ --argjson protocol "${PREDECESSOR_REHOME_PROTOCOL}" \ - --argjson drainingOk "$(test "${DEPLOY_MODE}" = rollback \ - && test "${ROLLBACK_RESUME}" != true && echo true || echo false)" \ + --argjson drainingOk "${PREDECESSOR_DRAINING_OK}" \ '[ if .role != "cell" then "role" else empty end, if .cellId != $cell then "cellId" else empty end, @@ -440,16 +523,6 @@ jobs: fi [[ "${SOURCE_INCARNATION}" =~ ^[0-9a-f-]{36}$ ]] echo "SOURCE_INCARNATION=${SOURCE_INCARNATION}" >> "${GITHUB_ENV}" - # Rollback is the documented recovery from a failed canary, which - # leaves the cell migration-only (and possibly still marked - # draining); apply and verify still require a pristine general cell. - if test "${DEPLOY_MODE}" = rollback; then - PRECHECK_ADMISSION=general-or-migration-only - PRECHECK_DRAINING=either - else - PRECHECK_ADMISSION=general - PRECHECK_DRAINING=forbidden - fi node dev/scripts/verify-relay-capacity-transition.mjs \ --director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \ --cell-id "${TARGET_CELL_ID}" --hard-cap "${EXPECTED_HARD_CAP}" \ @@ -475,18 +548,25 @@ jobs: --director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \ --cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode isolate)" echo "${ISOLATE_RESULT}" + # Isolating a migration-only cell must be a read-only no-op; a change here would + # mean the live class is not the one this wave planned around. + if test "${ENTRY_ADMISSION}" = migration-only; then + jq -e '.changed == false' <<< "${ISOLATE_RESULT}" >/dev/null + fi ISOLATE_GENERATION="$(jq -er '.generation' <<< "${ISOLATE_RESULT}")" echo "SELECTOR_GENERATION_AFTER_ISOLATE=${ISOLATE_GENERATION}" >> "${GITHUB_ENV}" node dev/scripts/prepare-relay-production-capacity-canary.mjs \ --director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \ - --cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode drain + --cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode drain \ + --pace-window-ms "${DRAIN_PACE_WINDOW_MS}" + # The wait has to outlast the pacing window as well as the leases it waits on. node dev/scripts/verify-relay-capacity-transition.mjs \ --director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \ --cell-id "${TARGET_CELL_ID}" --hard-cap "${EXPECTED_HARD_CAP}" \ --unobserved-bound "${EXPECTED_UNOBSERVED_BOUND}" \ --heartbeat either --admission migration-only --draining required \ - --activity restart-safe --expected-image-digests "${CURRENT_IMAGE_DIGEST}" \ - --timeout-ms 900000 + --activity restart-safe --expected-image-digests "${PREDECESSOR_IMAGE_DIGEST}" \ + --timeout-ms 1020000 - id: capacity-auth if: ${{ inputs.mode != 'verify' }} @@ -499,6 +579,7 @@ jobs: if: ${{ inputs.mode != 'verify' && env.ROLLBACK_RESUME == 'true' }} shell: bash env: + CAPACITY_SERVICE_ACCOUNT: ${{ vars.PRODUCTION_GCP_RELAY_CAPACITY_SERVICE_ACCOUNT }} DIRECTOR_RUNTIME_SERVICE_ACCOUNT: ${{ vars.PRODUCTION_GCP_RELAY_DIRECTOR_RUNTIME_SERVICE_ACCOUNT }} run: | # A cell on the root pool default emits no pool line, so pin one only where it exists. @@ -544,6 +625,7 @@ jobs: --unobserved-bound "${EXPECTED_UNOBSERVED_BOUND}" \ --image "${DESIRED_IMAGE}" \ --rollback-image "${DESIRED_IMAGE}" \ + --capacity-service-account "${CAPACITY_SERVICE_ACCOUNT}" \ --rehome-director-service-account "${DIRECTOR_RUNTIME_SERVICE_ACCOUNT}" \ --rehome-audience https://relay.onorca.dev/v1/admin/host-drain \ --regional-rehome-protocol "${DESIRED_REHOME_PROTOCOL}" \ @@ -571,20 +653,37 @@ jobs: "-target=google_compute_instance_template.relay_gce_cell[\"${TARGET_CELL_ID}\"]" \ "-target=google_compute_instance_group_manager.relay_gce_cell[\"${TARGET_CELL_ID}\"]" \ -out="${RUNNER_TEMP}/relay-same-cap.tfplan" - terraform -chdir=infra/terraform show -json "${RUNNER_TEMP}/relay-same-cap.tfplan" \ + PLAN_REVIEW="$(terraform -chdir=infra/terraform show -json \ + "${RUNNER_TEMP}/relay-same-cap.tfplan" \ | node dev/scripts/validate-relay-capacity-plan.mjs \ --mode same-cap-cell --cell-id "${TARGET_CELL_ID}" \ --hard-cap "${EXPECTED_HARD_CAP}" \ --unobserved-bound "${EXPECTED_UNOBSERVED_BOUND}" --image "${DESIRED_IMAGE}" \ - --rollback-image "${IMAGE_REPOSITORY}@${CURRENT_IMAGE_DIGEST}" \ + --rollback-image "${PLAN_ROLLBACK_IMAGE}" \ + --capacity-service-account "${CAPACITY_SERVICE_ACCOUNT}" \ --rehome-director-service-account "${DIRECTOR_RUNTIME_SERVICE_ACCOUNT}" \ --rehome-audience https://relay.onorca.dev/v1/admin/host-drain \ --regional-rehome-protocol "${DESIRED_REHOME_PROTOCOL}" \ - "${POOL_ARGUMENTS[@]}" + "${POOL_ARGUMENTS[@]}")" + echo "${PLAN_REVIEW}" terraform -chdir=infra/terraform apply -auto-approve \ "${RUNNER_TEMP}/relay-same-cap.tfplan" gcloud compute instance-groups managed wait-until "${MIG_NAME}" --stable \ --project "${GCP_PROJECT_ID}" --zone "${TARGET_ZONE}" --timeout 900 + # A stranded cell already runs the reviewed template, so the apply above replaces + # no instance and the drain flag, which only a restart clears, would survive the + # whole wave. Roll the MIG explicitly on exactly the policy a template change uses. + # Every field is passed: gcloud persists these into the MIG's update policy, and it + # defaults the method to substitute on a group with no stateful config, so omitting + # one drifts the policy off the reviewed one and fails every later targeted plan. + if test "${ROLLBACK_STAGE}" = stranded \ + && test "$(jq -er '.changes' <<< "${PLAN_REVIEW}")" = 0; then + gcloud compute instance-groups managed rolling-action replace "${MIG_NAME}" \ + --project "${GCP_PROJECT_ID}" --zone "${TARGET_ZONE}" \ + --replacement-method recreate --max-surge 0 --max-unavailable 1 + gcloud compute instance-groups managed wait-until "${MIG_NAME}" --stable \ + --project "${GCP_PROJECT_ID}" --zone "${TARGET_ZONE}" --timeout 900 + fi - id: post-auth if: ${{ inputs.mode != 'verify' }} @@ -659,28 +758,36 @@ jobs: --director-origin "${DIRECTOR_ORIGIN}" --cell-id "${TARGET_CELL_ID}" \ --cell-incarnation "${TARGET_INCARNATION}" - - name: Restore only the verified selected cell to general admission + - name: Restore only the verified selected cell to its entry admission if: ${{ inputs.mode != 'verify' }} env: ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.post-auth.outputs.id_token }} run: | echo "MUTATION_STARTED=true" >> "${GITHUB_ENV}" - ACTIVATE_RESULT="$(node dev/scripts/prepare-relay-production-capacity-canary.mjs \ + # Activating a migration-only cell would promote it to a serving cell for good, so + # restore it with the idempotent isolate that reports the authoritative generation. + if test "${ENTRY_ADMISSION}" = migration-only; then + RESTORE_MODE=isolate + else + RESTORE_MODE=activate + fi + RESTORE_RESULT="$(node dev/scripts/prepare-relay-production-capacity-canary.mjs \ --director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \ - --cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode activate)" - echo "${ACTIVATE_RESULT}" - SELECTOR_GENERATION_AFTER_ACTIVATE="$(jq -er '.generation' \ - <<< "${ACTIVATE_RESULT}")" + --cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode "${RESTORE_MODE}")" + echo "${RESTORE_RESULT}" + SELECTOR_GENERATION_AFTER_RESTORE="$(jq -er '.generation' \ + <<< "${RESTORE_RESULT}")" node dev/scripts/verify-relay-capacity-transition.mjs \ --director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \ --cell-id "${TARGET_CELL_ID}" --hard-cap "${EXPECTED_HARD_CAP}" \ --unobserved-bound "${EXPECTED_UNOBSERVED_BOUND}" \ - --heartbeat fresh --admission general --draining forbidden --activity allowed \ + --heartbeat fresh --admission "${ENTRY_ADMISSION}" \ + --draining forbidden --activity allowed \ --expected-image-digests "${DESIRED_IMAGE_DIGEST}" \ --regional-rehome-protocol "${DESIRED_REHOME_PROTOCOL}" node dev/scripts/operate-relay-regional-rehome.mjs \ --mode inspect --director-origin "${DIRECTOR_ORIGIN}" \ - --expected-selector-generation "${SELECTOR_GENERATION_AFTER_ACTIVATE}" \ + --expected-selector-generation "${SELECTOR_GENERATION_AFTER_RESTORE}" \ --expected-existing-only-cells "${EXPECTED_EXISTING_ONLY_CELLS}" \ --expected-migration-only-cells "${RESTORED_MIGRATION_CELLS}" \ --expected-general-cells "${RESTORED_GENERAL_CELLS}" \ diff --git a/.github/workflows/cloud-deploy-relay-production-same-cap.yml b/.github/workflows/cloud-deploy-relay-production-same-cap.yml index fa2cae818b2..421fa080072 100644 --- a/.github/workflows/cloud-deploy-relay-production-same-cap.yml +++ b/.github/workflows/cloud-deploy-relay-production-same-cap.yml @@ -181,14 +181,20 @@ jobs: if: ${{ inputs.mode == 'batch-apply' }} env: CANARY_RUN_ID: ${{ inputs.canary-run-id }} + CELL_IDS: ${{ inputs.cell-ids }} + TARGET_DIGEST: ${{ inputs.target-image-digest }} + ROLLBACK_DIGEST: ${{ inputs.rollback-image-digest }} + SELECTOR_GENERATION: ${{ inputs.expected-selector-generation }} + REHOME_GENERATION: ${{ inputs.expected-rehome-generation }} run: | node dev/scripts/relay-production-same-cap-wave.mjs verify-canary \ --file "${RUNNER_TEMP}/relay-same-cap-canary/authority.json" \ --commit-sha "${GITHUB_SHA}" --run-id "${CANARY_RUN_ID}" \ - --target-digest "${{ inputs.target-image-digest }}" \ - --rollback-digest "${{ inputs.rollback-image-digest }}" \ - --selector-generation "${{ inputs.expected-selector-generation }}" \ - --rehome-generation "${{ inputs.expected-rehome-generation }}" + --cell-ids "${CELL_IDS}" \ + --target-digest "${TARGET_DIGEST}" \ + --rollback-digest "${ROLLBACK_DIGEST}" \ + --selector-generation "${SELECTOR_GENERATION}" \ + --rehome-generation "${REHOME_GENERATION}" - name: Reject previously consumed aggregate safety evidence if: ${{ inputs.mode != 'verify' && inputs.gate-override-confirmation == '' }} diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 1e94add89ad..730129d674d 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -96,6 +96,13 @@ jobs: - name: Typecheck run: pnpm typecheck + # Why a ratchet and not the raw typecheck: mobile/tsconfig.json excludes test files, so until + # tsconfig.test.json existed nothing checked them, and at introduction 127 of the 632 had + # drifted. This fails when a test file that checks today stops checking, when a test leaves + # the program, and on @ts-nocheck; the baseline may only shrink. + - name: Typecheck tests (ratchet) + run: pnpm run check:tests-typecheck + - name: Test run: pnpm test diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 27e14c930e3..e2112de521b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -780,6 +780,12 @@ jobs: - name: Project web client from renderer build run: pnpm run build:web-from-renderer + # Why here and not inside "Build package inputs": this job assembles packaging inputs step by + # step instead of calling build:release, and electron-builder's beforePack guard hard-fails + # without out/mobile-web. + - name: Build mobile web bundle + run: pnpm run build:mobile-web + - name: Build native components run: pnpm run build:native diff --git a/cloud/apps/relay-ops/src/incident-live-preflight-cli.test.ts b/cloud/apps/relay-ops/src/incident-live-preflight-cli.test.ts index 532cf5279eb..587143ed7e0 100644 --- a/cloud/apps/relay-ops/src/incident-live-preflight-cli.test.ts +++ b/cloud/apps/relay-ops/src/incident-live-preflight-cli.test.ts @@ -738,6 +738,42 @@ describe('relay incident live preflight', () => { expect(seen[0]!.generation).toBe(5) }) + it('offsets by the wave delta the cell class declares', async () => { + const generationFor = async (args: string[]) => { + const seen: AdmissionSelector[] = [] + await expect(runIncidentLivePreflight(args, { + now: () => now, + collect: async (expected) => { + seen.push(expected) + const next = canonicalSample(expected.generation) + next.expectedSelector = expected + return next + } + })).resolves.toBeUndefined() + return seen[0]!.generation + } + // A migration-only cell's wave isolates and restores nothing, so no predecessor moved it. + expect(await generationFor( + overrideArgs(['--wave-index', '2', '--selector-wave-delta', '0']) + )).toBe(1) + expect(await generationFor( + overrideArgs(['--wave-index', '2', '--selector-wave-delta', '2']) + )).toBe(5) + }) + + it('rejects a selector wave delta no cell class produces', async () => { + for (const delta of ['1', '3', '4', '', '-0', '02']) { + await expect(runIncidentLivePreflight( + overrideArgs(['--selector-wave-delta', delta]), + { now: () => now } + )).rejects.toThrow('usage:') + } + await expect(runIncidentLivePreflight( + overrideArgs(['--selector-wave-delta', '0', '--selector-wave-delta', '0']), + { now: () => now } + )).rejects.toThrow('usage:') + }) + it('pins the strictest migration policy', async () => { // An inactive migration target is tolerable only under recover-forward, // and an override cannot elect that policy, so this must still fail. diff --git a/cloud/apps/relay-ops/src/incident-live-preflight-cli.ts b/cloud/apps/relay-ops/src/incident-live-preflight-cli.ts index d168fe69c46..5183286e302 100644 --- a/cloud/apps/relay-ops/src/incident-live-preflight-cli.ts +++ b/cloud/apps/relay-ops/src/incident-live-preflight-cli.ts @@ -31,6 +31,8 @@ const MONITOR_EVIDENCE_MAX_AGE_MS = 10 * 60_000 // Matches the same-cap cell job timeout-minutes; bounds each predecessor wave. const WAVE_PREDECESSOR_TIMEOUT_MS = 75 * 60_000 const WAVE_INDEX_PATTERN = /^[0-3]$/ +// 2 for a general cell's isolate-and-restore wave, 0 for a migration-only cell's no-op pair. +const SELECTOR_WAVE_DELTA_PATTERN = /^[02]$/ export function livePreflightGcloud( gcloud: ReturnType, @@ -93,13 +95,16 @@ function describeFailure(failure: IncidentFailure): string { } const PREFLIGHT_USAGE = - 'usage: --state-file [--wave-index <0-3>] [--retry-freshness]' + + 'usage: --state-file [--wave-index <0-3>]' + + ' [--selector-wave-delta <0|2>] [--retry-freshness]' + ' | --no-monitor-state --expected-selector-generation ' + - ' --selector-membership-file [--wave-index <0-3>]' + ' --selector-membership-file [--wave-index <0-3>]' + + ' [--selector-wave-delta <0|2>]' const VALUE_OPTIONS = new Set([ '--state-file', '--wave-index', + '--selector-wave-delta', '--expected-selector-generation', '--selector-membership-file' ]) @@ -241,6 +246,8 @@ export async function runIncidentLivePreflight( const parsed = parsePreflightArgs(argv) const waveIndex = parsed.options.get('--wave-index') ?? '0' if (!WAVE_INDEX_PATTERN.test(waveIndex)) throw new Error(PREFLIGHT_USAGE) + const selectorWaveDelta = parsed.options.get('--selector-wave-delta') ?? '2' + if (!SELECTOR_WAVE_DELTA_PATTERN.test(selectorWaveDelta)) throw new Error(PREFLIGHT_USAGE) const now = dependencies.now ?? Date.now const plan = parsed.flags.has('--no-monitor-state') ? await overridePreflightPlan(parsed.options, now()) @@ -252,14 +259,16 @@ export async function runIncidentLivePreflight( dependencies.environment ) // Each predecessor same-cap apply wave reversibly isolates and restores its - // cell, advancing the selector generation by exactly 2 with membership - // unchanged (rollback is single-cell, so it never reaches a later wave), so - // the live selector comparison must expect the wave-adjusted generation. + // cell with membership unchanged (rollback is single-cell, so it never reaches + // a later wave), so the live selector comparison must expect the wave-adjusted + // generation. A general cell advances it by 2; a migration-only cell is already + // isolated and stays that way, so its wave advances it by 0. A wave is never + // mixed, so one delta covers every predecessor. const collectOptions = { environment: plan.environment, expectedSelector: { ...plan.expectedSelector, - generation: plan.expectedSelector.generation + 2 * Number(waveIndex) + generation: plan.expectedSelector.generation + Number(selectorWaveDelta) * Number(waveIndex) }, ...(dependencies.now ? { now: dependencies.now } : {}) } diff --git a/cloud/apps/relay/src/app.ts b/cloud/apps/relay/src/app.ts index d60e2fd264c..978e544f5ff 100644 --- a/cloud/apps/relay/src/app.ts +++ b/cloud/apps/relay/src/app.ts @@ -58,6 +58,8 @@ const RelayCellConnectionHardCapSchema = z.custom( const ASSIGNMENT_REJECTION_LOG_WINDOW_MS = 10_000 const REGION_CATALOG_CACHE_MS = 30_000 +// A drain that outlives the roll step it belongs to is an outage, not a pacing win. +const DRAIN_PACE_WINDOW_MAX_MS = 5 * 60 * 1_000 type AdmissionRejectionLogEntry = { route: 'assign' | 'resolve' @@ -72,7 +74,7 @@ export function createRelayApp( operations: { store: RelayCredentialStore assignments: RelayAssignmentStore - drain: (graceMs: number) => void + drain: (graceMs: number, options?: { paceWindowMs?: number }) => void idleRehome?: (input: IdleRegionalRehomeRequest & { cohortPercent: number directorSafety: RegionalRehomeSafetySnapshot @@ -488,12 +490,18 @@ export function createRelayApp( return context.json({ error: 'invalid_token' }, 401) } const body = z - .object({ v: z.literal(1), graceMs: z.number().int().nonnegative().max(60 * 60 * 1000) }) + .object({ + v: z.literal(1), + graceMs: z.number().int().nonnegative().max(60 * 60 * 1000), + // Spreads the drain sends, and so the re-dials, over this window. + paceWindowMs: z.number().int().nonnegative().max(DRAIN_PACE_WINDOW_MAX_MS).optional() + }) .strict() .safeParse(await context.req.json().catch(() => null)) if (!body.success) return context.json({ error: 'invalid_request' }, 400) - operations.drain(body.data.graceMs) - return context.json({ ok: true }) + const paceWindowMs = body.data.paceWindowMs ?? 0 + operations.drain(body.data.graceMs, { paceWindowMs }) + return context.json({ ok: true, paceWindowMs }) }) app.post('/v1/admin/host-idle-rehome', async (context) => { if (config.role !== 'cell' || !operations.idleRehome) { diff --git a/cloud/apps/relay/src/assignment-store.ts b/cloud/apps/relay/src/assignment-store.ts index bb7812dda86..8c12872effc 100644 --- a/cloud/apps/relay/src/assignment-store.ts +++ b/cloud/apps/relay/src/assignment-store.ts @@ -45,6 +45,15 @@ import { ASSIGNMENT_CONNECTION_HEADROOM_QUERY } from './assignment-connection-headroom-query.js' import { AssignmentIdentityQueue } from './assignment-identity-queue.js' +import { + CONTROL_RENEWAL_BATCH_SQL, + CONTROL_RENEWAL_STATEMENT_OUTCOMES, + controlRenewalBatchParams, + orderedControlRenewalRows, + readControlRenewalOutcomes, + type ControlRenewalOutcome, + type ControlRenewalRequest +} from './control-renewal-statement.js' import { REGIONAL_REHOME_DEFAULT_HOST_COOLDOWN_MS } from './database.js' @@ -101,21 +110,8 @@ type RelayAssignmentStoreOptions = { recordControlRenewal?: (durationMs: number, outcome: ControlRenewalOutcome) => void } -export type ControlRenewalOutcome = - | 'renewed' - | 'assignment_not_found' - | 'activity_cell_not_authoritative' - | 'control_activity_not_found' - | 'control_activity_moved' - | 'database_error' +export type { ControlRenewalOutcome, ControlRenewalRequest } -const CONTROL_RENEWAL_OUTCOMES = new Set([ - 'renewed', - 'assignment_not_found', - 'activity_cell_not_authoritative', - 'control_activity_not_found', - 'control_activity_moved' -]) export type RelayAssignment = AssignmentIdentity & { cellId: string cellUrl: string @@ -3478,132 +3474,149 @@ export class RelayAssignmentStore { }) } + // Kept as the single-row contract for callers and tests: resolves on a + // renewal and throws the outcome (or the driver's own error) otherwise. async renewControlActivity( identity: AssignmentIdentity, input: { activityId: string; cellId: string; expiresAt: number } ): Promise { validateActivityId(input.activityId) const now = this.now() - const maximumExpiresAt = - now + - ASSIGNMENT_LIMITS.activityLeaseMs + - RELAY_PROTOCOL_LIMITS.controlPingIntervalMs * 2 - if ( - !Number.isSafeInteger(input.expiresAt) || - input.expiresAt <= now || - input.expiresAt > maximumExpiresAt - ) { + if (!controlRenewalExpiryIsValid(input.expiresAt, now)) { throw new Error('invalid_activity_expiry') } const startedAt = performance.now() let outcome: ControlRenewalOutcome = 'database_error' try { - outcome = - this.database.dialect === 'postgres' - ? await this.renewPostgresControlActivity(identity, input, now) - : await this.renewTransactionalControlActivity(identity, input, now) + outcome = await this.renewOneControlActivity({ identity, ...input }, now) if (outcome !== 'renewed') throw new Error(outcome) } catch (error) { - const message = String((error as { message?: unknown }).message) - if (CONTROL_RENEWAL_OUTCOMES.has(message as ControlRenewalOutcome)) { - outcome = message as ControlRenewalOutcome - } + outcome = controlRenewalOutcomeOfError(error) throw error } finally { this.recordControlRenewal?.(performance.now() - startedAt, outcome) } } - private async renewPostgresControlActivity( - identity: AssignmentIdentity, - input: { activityId: string; cellId: string; expiresAt: number }, + // Renews every due control lease on a cell in one write transaction, returning + // one outcome per input row in input order. Never throws for a multi-row batch: + // a caller routes its own session on its own outcome. + async renewControlActivities( + rows: readonly ControlRenewalRequest[] + ): Promise { + const now = this.now() + const outcomes = new Array(rows.length) + const accepted: Array = [] + for (const [index, row] of rows.entries()) { + const rejection = controlRenewalRejection(row, now) + if (rejection) outcomes[index] = rejection + else accepted.push({ ...row, index }) + } + const startedAt = performance.now() + try { + if (accepted.length === 0) return outcomes + let results: ControlRenewalOutcome[] + try { + results = await this.executeControlRenewals(accepted, now) + } catch (error) { + if (rows.length === 1) { + outcomes[accepted[0]!.index] = controlRenewalOutcomeOfError(error) + throw error + } + results = accepted.map(() => 'database_error') + } + for (const [position, row] of accepted.entries()) outcomes[row.index] = results[position]! + return outcomes + } finally { + // Every path, so a rethrown lone renewal and an all-invalid batch are + // counted the same as a batch that reached PostgreSQL. + const durationMs = performance.now() - startedAt + for (const outcome of outcomes) this.recordControlRenewal?.(durationMs, outcome) + } + } + + private async executeControlRenewals( + rows: readonly ControlRenewalRequest[], + now: number + ): Promise { + if (rows.length === 1) return [await this.renewOneControlActivity(rows[0]!, now)] + if (this.database.dialect !== 'postgres') { + // Correctness over throughput: the SQLite writer is serialized anyway, and + // this is the dialect the unit suites run on. + return await this.renewControlActivitiesInSeries(rows, now) + } + const ordered = orderedControlRenewalRows(rows.map((row, index) => ({ ...row, index }))) + const outcomes = new Array(rows.length) + try { + const parsed = readControlRenewalOutcomes( + await this.database.query( + CONTROL_RENEWAL_BATCH_SQL, + controlRenewalBatchParams(ordered, now) + ), + ordered.length + ) + for (const [position, row] of ordered.entries()) outcomes[row.index] = parsed[position]! + return outcomes + } catch (error) { + // One statement means one contended assignment row can fail the whole + // batch, so a failure degrades to the per-host statements this replaced + // rather than costing every other host on the cell its renewal. + console.warn( + JSON.stringify({ + event: 'orca_relay_control_renewal_batch_failed', + rows: ordered.length, + message: String((error as { message?: unknown }).message) + }) + ) + await Promise.all( + ordered.map(async (row) => { + try { + outcomes[row.index] = await this.renewOneControlActivity(row, now) + } catch { + outcomes[row.index] = 'database_error' + } + }) + ) + return outcomes + } + } + + private async renewControlActivitiesInSeries( + rows: readonly ControlRenewalRequest[], + now: number + ): Promise { + const outcomes: ControlRenewalOutcome[] = [] + for (const row of rows) { + try { + outcomes.push(await this.renewOneControlActivity(row, now)) + } catch { + outcomes.push('database_error') + } + } + return outcomes + } + + // Returns the outcome; a driver or pool failure reaches the caller unchanged. + private async renewOneControlActivity( + row: ControlRenewalRequest, now: number ): Promise { - const row = ( - await this.database.query( - `WITH assignment_state AS MATERIALIZED ( - SELECT cell_id, assignment_epoch - FROM relay_assignments - WHERE user_id = ? AND relay_host_id = ? - FOR UPDATE - ), migration_state AS MATERIALIZED ( - SELECT migration.assignment_epoch - FROM relay_assignment_migrations migration - JOIN assignment_state assignment - ON migration.target_cell_id = assignment.cell_id - AND migration.assignment_epoch = assignment.assignment_epoch - WHERE migration.user_id = ? AND migration.relay_host_id = ? - AND migration.source_cell_id = ? - AND migration.completed_at IS NULL AND migration.aborted_at IS NULL - FOR UPDATE OF migration - ), authorization_state AS MATERIALIZED ( - SELECT 1 AS authorized - FROM assignment_state assignment - WHERE assignment.cell_id = ? OR EXISTS (SELECT 1 FROM migration_state) - ), lease_state AS MATERIALIZED ( - SELECT lease.activity_kind, lease.cell_id - FROM relay_assignment_activity_leases lease - CROSS JOIN authorization_state - WHERE lease.user_id = ? AND lease.relay_host_id = ? AND lease.activity_id = ? - FOR UPDATE OF lease - ), renewed_lease AS ( - UPDATE relay_assignment_activity_leases lease - SET expires_at = GREATEST(lease.expires_at, ?), - updated_at = GREATEST(lease.updated_at, ?) - FROM lease_state state - WHERE lease.user_id = ? AND lease.relay_host_id = ? AND lease.activity_id = ? - AND state.activity_kind = 'control' AND state.cell_id = ? - RETURNING 1 - ), renewed_assignment AS ( - UPDATE relay_assignments assignment - SET lease_expires_at = GREATEST(assignment.lease_expires_at, ?), - last_activity_at = GREATEST(assignment.last_activity_at, ?) - WHERE assignment.user_id = ? AND assignment.relay_host_id = ? - AND EXISTS (SELECT 1 FROM renewed_lease) - RETURNING 1 - ) - SELECT CASE - WHEN NOT EXISTS (SELECT 1 FROM assignment_state) - THEN 'assignment_not_found' - WHEN NOT EXISTS (SELECT 1 FROM authorization_state) - THEN 'activity_cell_not_authoritative' - WHEN NOT EXISTS (SELECT 1 FROM lease_state) - THEN 'control_activity_not_found' - WHEN EXISTS ( - SELECT 1 FROM lease_state - WHERE activity_kind <> 'control' OR cell_id <> ? - ) THEN 'control_activity_moved' - WHEN EXISTS (SELECT 1 FROM renewed_assignment) THEN 'renewed' - ELSE 'control_activity_not_found' - END AS outcome`, - [ - identity.userId, - identity.relayHostId, - identity.userId, - identity.relayHostId, - input.cellId, - input.cellId, - identity.userId, - identity.relayHostId, - input.activityId, - input.expiresAt, - now, - identity.userId, - identity.relayHostId, - input.activityId, - input.cellId, - input.expiresAt, - now, - identity.userId, - identity.relayHostId, - input.cellId - ] - ) - )[0] - if (!row) throw new Error('missing_control_renewal_outcome') - const outcome = text(row, 'outcome') as ControlRenewalOutcome - if (!CONTROL_RENEWAL_OUTCOMES.has(outcome)) throw new Error('invalid_control_renewal_outcome') - return outcome + if (this.database.dialect === 'postgres') { + return readControlRenewalOutcomes( + await this.database.query( + CONTROL_RENEWAL_BATCH_SQL, + controlRenewalBatchParams([row], now) + ), + 1 + )[0]! + } + try { + return await this.renewTransactionalControlActivity(row.identity, row, now) + } catch (error) { + const message = String((error as { message?: unknown }).message) + if (!CONTROL_RENEWAL_STATEMENT_OUTCOMES.has(message as ControlRenewalOutcome)) throw error + return message as ControlRenewalOutcome + } } private async renewTransactionalControlActivity( @@ -7900,6 +7913,33 @@ function validateActivityId(activityId: string): void { if (!activityId || activityId.length > 256) throw new Error('invalid_activity_id') } +function controlRenewalExpiryIsValid(expiresAt: number, now: number): boolean { + const maximumExpiresAt = + now + ASSIGNMENT_LIMITS.activityLeaseMs + RELAY_PROTOCOL_LIMITS.controlPingIntervalMs * 2 + return Number.isSafeInteger(expiresAt) && expiresAt > now && expiresAt <= maximumExpiresAt +} + +// A renewal that threw still owes the metric an outcome: the message carries one +// when the statement decided it, and anything else is the driver failing. +function controlRenewalOutcomeOfError(error: unknown): ControlRenewalOutcome { + const message = String((error as { message?: unknown }).message) + return CONTROL_RENEWAL_STATEMENT_OUTCOMES.has(message as ControlRenewalOutcome) + ? (message as ControlRenewalOutcome) + : 'database_error' +} + +function controlRenewalRejection( + row: ControlRenewalRequest, + now: number +): ControlRenewalOutcome | null { + try { + validateActivityId(row.activityId) + } catch { + return 'invalid_activity_id' + } + return controlRenewalExpiryIsValid(row.expiresAt, now) ? null : 'invalid_activity_expiry' +} + function activityKind(row: SqlRow): AssignmentActivityKind { const value = text(row, 'activity_kind') if (!(value in ACTIVITY_REQUEST_UNITS)) throw new Error('invalid_activity_kind') diff --git a/cloud/apps/relay/src/control-lease-recovery-postgres.test.ts b/cloud/apps/relay/src/control-lease-recovery-postgres.test.ts index 2fa01c27df8..fed7f56b31b 100644 --- a/cloud/apps/relay/src/control-lease-recovery-postgres.test.ts +++ b/cloud/apps/relay/src/control-lease-recovery-postgres.test.ts @@ -3,6 +3,7 @@ import { ASSIGNMENT_LIMITS, RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import type WebSocket from 'ws' import { RelayAssignmentStore } from './assignment-store.js' +import { CONTROL_RENEWAL_BATCH_INTERVAL_MS } from './control-renewal-batch.js' import type { RelayConfig } from './config.js' import type { RelayCredentialStore } from './credential-store.js' import { openRelayDatabase, type RelayDatabase } from './database.js' @@ -133,6 +134,10 @@ describePostgres('expired control lease after a database outage', () => { internals.heartbeat(session) } + // A due renewal leaves the heartbeat as a batch enqueue, so a poll has to + // outlast the batch window before it can call the renewal missing. + const renewalPoll = { timeout: CONTROL_RENEWAL_BATCH_INTERVAL_MS + 4_000 } + const leaseRows = async (relayHostId: string) => await database.query( `SELECT activity_id, cell_id FROM relay_assignment_activity_leases @@ -149,7 +154,8 @@ describePostgres('expired control lease after a database outage', () => { .poll( async () => socket.close.mock.calls.length > 0 || - (await leaseRows(relayHostId)).length === expectedRows + (await leaseRows(relayHostId)).length === expectedRows, + renewalPoll ) .toBe(true) } @@ -212,7 +218,7 @@ describePostgres('expired control lease after a database outage', () => { ).rejects.toThrow('control_activity_moved') heartbeat(registry, session) - await expect.poll(() => socket.close.mock.calls.length).toBe(1) + await expect.poll(() => socket.close.mock.calls.length, renewalPoll).toBe(1) expect(socket.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.DRAINING, 'control activity moved') expect(await leaseRows(identity.relayHostId)).toEqual([ diff --git a/cloud/apps/relay/src/control-renewal-batch-store.test.ts b/cloud/apps/relay/src/control-renewal-batch-store.test.ts new file mode 100644 index 00000000000..e3798a19ae2 --- /dev/null +++ b/cloud/apps/relay/src/control-renewal-batch-store.test.ts @@ -0,0 +1,291 @@ +import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract' +import { describe, expect, it, vi } from 'vitest' +import { RelayAssignmentStore } from './assignment-store.js' +import { CONTROL_RENEWAL_BATCH_SQL } from './control-renewal-statement.js' +import type { ControlRenewalOutcome } from './control-renewal-statement.js' +import { + openInMemoryRelayDatabase, + type RelayDatabase, + type SqlRow +} from './database.js' + +const now = 1_900_000_000_000 +const expiresAt = now + 105_000 + +function renewal(userId: string, relayHostId: string, expiry = expiresAt) { + return { + identity: { userId, relayHostId }, + activityId: 'control:cell-a:1', + cellId: 'cell-a', + expiresAt: expiry + } +} + +// A PostgreSQL-dialect database that answers the renewal statement without a +// server, so the statement count and its parameter arrays are observable. +class RenewalStatementProbe implements RelayDatabase { + readonly dialect = 'postgres' as const + readonly statements: Array<{ sql: string; params: unknown[] }> = [] + failuresRemaining = 0 + + constructor(private readonly outcomeFor: (userId: string) => ControlRenewalOutcome) {} + + async query(sql: string, params: unknown[] = []): Promise { + this.statements.push({ sql, params }) + if (this.failuresRemaining > 0) { + this.failuresRemaining -= 1 + throw new Error('canceling statement due to statement timeout') + } + const userIds = params[0] as string[] + return userIds.map((userId, index) => ({ + row_index: String(index + 1), + outcome: this.outcomeFor(userId) + })) + } + + async queryLocked(): Promise { + throw new Error('unexpected_locked_query') + } + + async transaction(): Promise { + // Renewals must never open one: that is the write transaction per host this + // batch exists to remove. + throw new Error('unexpected_transaction') + } + + async close(): Promise {} +} + +describe('batched control renewals on PostgreSQL', () => { + it('spends one statement on every host that came due', async () => { + const probe = new RenewalStatementProbe(() => 'renewed') + const store = new RelayAssignmentStore(probe, () => now) + + const outcomes = await store.renewControlActivities([ + renewal('user-a', 'host000000000001'), + renewal('user-a', 'host000000000002'), + renewal('user-b', 'host000000000003') + ]) + + expect(outcomes).toEqual(['renewed', 'renewed', 'renewed']) + expect(probe.statements).toHaveLength(1) + expect(probe.statements[0]!.sql).toBe(CONTROL_RENEWAL_BATCH_SQL) + expect(probe.statements[0]!.params[0]).toEqual(['user-a', 'user-a', 'user-b']) + expect(probe.statements[0]!.params[4]).toEqual([expiresAt, expiresAt, expiresAt]) + }) + + it('locks assignment rows in primary-key order and still answers in input order', async () => { + const probe = new RenewalStatementProbe((userId) => + userId === 'user-b' ? 'control_activity_moved' : 'renewed' + ) + const store = new RelayAssignmentStore(probe, () => now) + + const outcomes = await store.renewControlActivities([ + renewal('user-c', 'host000000000003'), + renewal('user-a', 'host000000000002'), + renewal('user-b', 'host000000000001'), + renewal('user-a', 'host000000000001') + ]) + + // (user_id, relay_host_id) is the primary key of relay_assignments, and the + // statement's ORDER BY repeats it: no batch can queue against another in a + // different sequence. + expect(probe.statements[0]!.params[0]).toEqual(['user-a', 'user-a', 'user-b', 'user-c']) + expect(probe.statements[0]!.params[1]).toEqual([ + 'host000000000001', + 'host000000000002', + 'host000000000001', + 'host000000000003' + ]) + expect(outcomes).toEqual([ + 'renewed', + 'renewed', + 'control_activity_moved', + 'renewed' + ]) + }) + + it('keeps a malformed request out of the statement and fails only that row', async () => { + const probe = new RenewalStatementProbe(() => 'renewed') + const store = new RelayAssignmentStore(probe, () => now) + + const outcomes = await store.renewControlActivities([ + renewal('user-a', 'host000000000001'), + renewal('user-a', 'host000000000002', now + ASSIGNMENT_LIMITS.activityLeaseMs * 10), + { ...renewal('user-a', 'host000000000003'), activityId: '' }, + renewal('user-a', 'host000000000004') + ]) + + expect(outcomes).toEqual([ + 'renewed', + 'invalid_activity_expiry', + 'invalid_activity_id', + 'renewed' + ]) + expect(probe.statements[0]!.params[1]).toEqual(['host000000000001', 'host000000000004']) + }) + + it('degrades to one statement per host when the batch statement fails', async () => { + const probe = new RenewalStatementProbe(() => 'renewed') + probe.failuresRemaining = 1 + const store = new RelayAssignmentStore(probe, () => now) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + try { + const outcomes = await store.renewControlActivities([ + renewal('user-a', 'host000000000001'), + renewal('user-a', 'host000000000002') + ]) + + expect(outcomes).toEqual(['renewed', 'renewed']) + expect(probe.statements).toHaveLength(3) + expect(probe.statements[1]!.params[1]).toEqual(['host000000000001']) + expect(probe.statements[2]!.params[1]).toEqual(['host000000000002']) + expect(JSON.parse(String(warn.mock.calls[0]![0]))).toMatchObject({ + event: 'orca_relay_control_renewal_batch_failed', + rows: 2 + }) + } finally { + warn.mockRestore() + } + }) + + it('reports a host that fails its own fallback statement without touching the rest', async () => { + const probe = new RenewalStatementProbe(() => 'renewed') + probe.failuresRemaining = 2 + const store = new RelayAssignmentStore(probe, () => now) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + try { + const outcomes = await store.renewControlActivities([ + renewal('user-a', 'host000000000001'), + renewal('user-a', 'host000000000002') + ]) + + expect(outcomes.filter((outcome) => outcome === 'renewed')).toHaveLength(1) + expect(outcomes.filter((outcome) => outcome === 'database_error')).toHaveLength(1) + } finally { + warn.mockRestore() + } + }) + + it('reports a contended assignment row apart from a missing one', async () => { + const probe = new RenewalStatementProbe((userId) => + userId === 'user-b' ? 'assignment_lock_unavailable' : 'renewed' + ) + const store = new RelayAssignmentStore(probe, () => now) + + const outcomes = await store.renewControlActivities([ + renewal('user-a', 'host000000000001'), + renewal('user-b', 'host000000000002') + ]) + + // Retryable: SKIP LOCKED passed over the row rather than queueing the whole + // flush behind whoever held it. + expect(outcomes).toEqual(['renewed', 'assignment_lock_unavailable']) + }) + + it('counts a lone renewal that threw before rethrowing it', async () => { + const probe = new RenewalStatementProbe(() => 'renewed') + probe.failuresRemaining = 1 + const recordControlRenewal = vi.fn() + const store = new RelayAssignmentStore(probe, () => now, { recordControlRenewal }) + + // A one-row flush keeps the pre-batch contract and rethrows, but the metric + // still owes an outcome for the attempt. + await expect( + store.renewControlActivities([renewal('user-a', 'host000000000001')]) + ).rejects.toThrow('statement timeout') + + expect(recordControlRenewal).toHaveBeenCalledTimes(1) + expect(recordControlRenewal.mock.calls[0]![1]).toBe('database_error') + }) + + it('counts a batch in which every row was rejected before the statement', async () => { + const probe = new RenewalStatementProbe(() => 'renewed') + const recordControlRenewal = vi.fn() + const store = new RelayAssignmentStore(probe, () => now, { recordControlRenewal }) + + const outcomes = await store.renewControlActivities([ + { ...renewal('user-a', 'host000000000001'), activityId: '' }, + renewal('user-a', 'host000000000002', now - 1) + ]) + + expect(outcomes).toEqual(['invalid_activity_id', 'invalid_activity_expiry']) + expect(probe.statements).toHaveLength(0) + expect(recordControlRenewal.mock.calls.map((call) => call[1])).toEqual([ + 'invalid_activity_id', + 'invalid_activity_expiry' + ]) + }) + + it('counts one renewal metric per row against the flush latency', async () => { + const probe = new RenewalStatementProbe((userId) => + userId === 'user-b' ? 'assignment_not_found' : 'renewed' + ) + const recordControlRenewal = vi.fn() + const store = new RelayAssignmentStore(probe, () => now, { recordControlRenewal }) + + await store.renewControlActivities([ + renewal('user-a', 'host000000000001'), + renewal('user-b', 'host000000000002') + ]) + + expect(recordControlRenewal).toHaveBeenCalledTimes(2) + expect(recordControlRenewal.mock.calls.map((call) => call[1])).toEqual([ + 'renewed', + 'assignment_not_found' + ]) + }) +}) + +describe('batched control renewals on SQLite', () => { + it('renews every host through the transactional path', async () => { + let clock = now + const database = await openInMemoryRelayDatabase() + try { + const store = new RelayAssignmentStore(database, () => clock) + await store.reconcileCells([ + { id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 } + ]) + const hosts = ['host000000000001', 'host000000000002'] + const requests = [] + for (const relayHostId of hosts) { + const identity = { userId: 'user-a', relayHostId } + const assignment = await store.assign(identity) + await store.activateControl(identity, { + cellId: assignment.cellId, + assignmentEpoch: assignment.assignmentEpoch, + generation: 1 + }) + requests.push({ + identity, + activityId: `control:${assignment.cellId}:1`, + cellId: assignment.cellId, + expiresAt: clock + 105_000 + }) + } + // A host with no assignment at all must not cost the others their renewal. + requests.push({ + identity: { userId: 'user-a', relayHostId: 'host000000000009' }, + activityId: 'control:cell-a:1', + cellId: 'cell-a', + expiresAt: clock + 105_000 + }) + clock += 1_000 + + const outcomes = await store.renewControlActivities(requests) + + expect(outcomes).toEqual(['renewed', 'renewed', 'assignment_not_found']) + const leases = await database.query( + `SELECT relay_host_id, expires_at FROM relay_assignment_activity_leases + WHERE user_id = ? ORDER BY relay_host_id ASC`, + ['user-a'] + ) + expect(leases.map((lease) => Number(lease.expires_at))).toEqual([ + now + 105_000, + now + 105_000 + ]) + } finally { + await database.close() + } + }) +}) diff --git a/cloud/apps/relay/src/control-renewal-batch.test.ts b/cloud/apps/relay/src/control-renewal-batch.test.ts new file mode 100644 index 00000000000..10178b88476 --- /dev/null +++ b/cloud/apps/relay/src/control-renewal-batch.test.ts @@ -0,0 +1,231 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + CONTROL_RENEWAL_BATCH_INTERVAL_MS, + CONTROL_RENEWAL_BATCH_MAX_ROWS, + ControlRenewalBatch, + type ControlRenewalFlush +} from './control-renewal-batch.js' +import type { + ControlRenewalOutcome, + ControlRenewalRequest +} from './control-renewal-statement.js' + +// Settles into the outcome the caller saw, attached at enqueue so a rejection is +// never momentarily unhandled. +function outcomeOf(renewal: Promise): Promise { + return renewal.then( + () => 'renewed', + (error: unknown) => String((error as { message?: unknown }).message) + ) +} + +function request( + host: string, + expiresAt = 1_000, + activityId = 'control:cell-a:1' +): ControlRenewalRequest { + return { + identity: { userId: 'user-a', relayHostId: host }, + activityId, + cellId: 'cell-a', + expiresAt + } +} + +describe('control renewal batch', () => { + beforeEach(() => vi.useFakeTimers()) + afterEach(() => { + vi.clearAllTimers() + vi.useRealTimers() + }) + + it('spends one call on every renewal that came due in the window', async () => { + const renew = vi.fn(async (rows: readonly ControlRenewalRequest[]) => + rows.map((): ControlRenewalOutcome => 'renewed') + ) + const batch = new ControlRenewalBatch(renew) + const settled = [ + batch.enqueue(request('host0000000000a1')), + batch.enqueue(request('host0000000000a2')), + batch.enqueue(request('host0000000000a3')) + ] + + expect(renew).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + await expect(Promise.all(settled)).resolves.toEqual([undefined, undefined, undefined]) + expect(renew).toHaveBeenCalledOnce() + expect(renew.mock.calls[0]![0].map((row) => row.identity.relayHostId)).toEqual([ + 'host0000000000a1', + 'host0000000000a2', + 'host0000000000a3' + ]) + }) + + it('routes each outcome back to the caller that asked for it', async () => { + const outcomes: ControlRenewalOutcome[] = [ + 'renewed', + 'assignment_not_found', + 'control_activity_moved' + ] + const batch = new ControlRenewalBatch(async () => outcomes) + const first = outcomeOf(batch.enqueue(request('host0000000000b1'))) + const second = outcomeOf(batch.enqueue(request('host0000000000b2'))) + const third = outcomeOf(batch.enqueue(request('host0000000000b3'))) + + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + + await expect(Promise.all([first, second, third])).resolves.toEqual([ + 'renewed', + 'assignment_not_found', + 'control_activity_moved' + ]) + }) + + it('flushes on reaching the row ceiling instead of waiting out the window', async () => { + const renew = vi.fn(async (rows: readonly ControlRenewalRequest[]) => + rows.map((): ControlRenewalOutcome => 'renewed') + ) + const batch = new ControlRenewalBatch(renew) + for (let row = 0; row < CONTROL_RENEWAL_BATCH_MAX_ROWS - 1; row++) { + void batch.enqueue(request(`host${String(row).padStart(12, '0')}`)) + } + expect(renew).not.toHaveBeenCalled() + + void batch.enqueue(request('host0000000000zz')) + await vi.advanceTimersByTimeAsync(0) + + expect(renew).toHaveBeenCalledOnce() + expect(renew.mock.calls[0]![0]).toHaveLength(CONTROL_RENEWAL_BATCH_MAX_ROWS) + // The window timer must not fire a second, empty statement. + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + expect(renew).toHaveBeenCalledOnce() + }) + + it('does not hold a new window behind a statement still in PostgreSQL', async () => { + let release!: (outcomes: ControlRenewalOutcome[]) => void + const renew = vi + .fn<(rows: readonly ControlRenewalRequest[]) => Promise>() + .mockImplementationOnce( + async () => await new Promise((resolve) => (release = resolve)) + ) + .mockResolvedValue(['renewed']) + const batch = new ControlRenewalBatch(renew) + const stalled = batch.enqueue(request('host0000000000c1')) + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + + const next = batch.enqueue(request('host0000000000c2')) + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + + expect(renew).toHaveBeenCalledTimes(2) + await expect(next).resolves.toBeUndefined() + release(['renewed']) + await expect(stalled).resolves.toBeUndefined() + }) + + it('reports the driver failure to every caller in the flush', async () => { + const batch = new ControlRenewalBatch(async () => { + throw new Error('pool timeout') + }) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + try { + const first = outcomeOf(batch.enqueue(request('host0000000000d1'))) + const second = outcomeOf(batch.enqueue(request('host0000000000d2'))) + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + + await expect(Promise.all([first, second])).resolves.toEqual([ + 'pool timeout', + 'pool timeout' + ]) + } finally { + warn.mockRestore() + } + }) + + it('supersedes a second attempt for one lease and answers both callers', async () => { + const renew = vi.fn(async (rows: readonly ControlRenewalRequest[]) => + rows.map((): ControlRenewalOutcome => 'renewed') + ) + const batch = new ControlRenewalBatch(renew) + const earlier = batch.enqueue(request('host0000000000e1', 1_000)) + const later = batch.enqueue(request('host0000000000e1', 2_000)) + + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + + expect(renew.mock.calls[0]![0]).toEqual([ + expect.objectContaining({ expiresAt: 2_000 }) + ]) + await expect(earlier).resolves.toBeUndefined() + await expect(later).resolves.toBeUndefined() + }) + + it('holds a second activity for one host back to the next flush', async () => { + const renew = vi.fn(async (rows: readonly ControlRenewalRequest[]) => + rows.map((): ControlRenewalOutcome => 'renewed') + ) + const batch = new ControlRenewalBatch(renew) + const first = batch.enqueue(request('host0000000000h1', 1_000, 'control:cell-a:1')) + const second = batch.enqueue(request('host0000000000h1', 1_000, 'control:cell-a:2')) + const other = batch.enqueue(request('host0000000000h2')) + + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + + // One statement updates a host's assignment row once, so the host appears in + // one flush only; the newer generation leads the next one. + expect(renew.mock.calls[0]![0].map((row) => row.activityId)).toEqual([ + 'control:cell-a:1', + 'control:cell-a:1' + ]) + await expect(Promise.all([first, other])).resolves.toEqual([undefined, undefined]) + + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + + expect(renew).toHaveBeenCalledTimes(2) + expect(renew.mock.calls[1]![0].map((row) => row.activityId)).toEqual(['control:cell-a:2']) + await expect(second).resolves.toBeUndefined() + }) + + it('stays quiet for a fast flush that renewed everything', async () => { + const flushes: ControlRenewalFlush[] = [] + const batch = new ControlRenewalBatch( + async () => ['renewed'], + () => ({ cellId: 'cell-a' }), + (flush) => flushes.push(flush) + ) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + try { + void batch.enqueue(request('host0000000000f1')) + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + + expect(warn).not.toHaveBeenCalled() + expect(flushes).toEqual([ + { rows: 1, durationMs: expect.any(Number), outcomes: { renewed: 1 } } + ]) + } finally { + warn.mockRestore() + } + }) + + it('logs one line with the outcome counts when a flush did not renew everything', async () => { + const batch = new ControlRenewalBatch( + async () => ['renewed', 'control_activity_not_found'], + () => ({ cellId: 'cell-a' }) + ) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + try { + void outcomeOf(batch.enqueue(request('host0000000000g1'))) + const missing = outcomeOf(batch.enqueue(request('host0000000000g2'))) + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) + await expect(missing).resolves.toBe('control_activity_not_found') + + expect(warn).toHaveBeenCalledOnce() + expect(JSON.parse(String(warn.mock.calls[0]![0]))).toMatchObject({ + event: 'orca_relay_control_renewal_flush', + cellId: 'cell-a', + rows: 2, + outcomes: { renewed: 1, control_activity_not_found: 1 } + }) + } finally { + warn.mockRestore() + } + }) +}) diff --git a/cloud/apps/relay/src/control-renewal-batch.ts b/cloud/apps/relay/src/control-renewal-batch.ts new file mode 100644 index 00000000000..984333b2ca6 --- /dev/null +++ b/cloud/apps/relay/src/control-renewal-batch.ts @@ -0,0 +1,148 @@ +import { performance } from 'node:perf_hooks' +import type { + ControlRenewalOutcome, + ControlRenewalRequest +} from './control-renewal-statement.js' + +// One flush per second turns the fleet's control-lease write rate into a +// function of the cell count rather than the host count: a cell's ~10 due +// renewals per second become one write transaction instead of ten. Well inside +// the 105s lease runway, so a host that misses a window is never at risk. +export const CONTROL_RENEWAL_BATCH_INTERVAL_MS = 1_000 +// Ceiling on the parameter arrays. Row locks live until the statement commits, +// so this is what bounds how long one flush holds them: measured at 11.5ms for +// 200 rows against a 20,000-row table, and 9.4ms with a host wedged in a +// per-host transaction. +export const CONTROL_RENEWAL_BATCH_MAX_ROWS = 200 +// A flush slower than this is the only latency worth a line; the metrics event +// carries the distribution. +const CONTROL_RENEWAL_SLOW_FLUSH_MS = 250 + +export type ControlRenewalFlush = { + rows: number + durationMs: number + outcomes: Record +} + +type PendingWaiter = { resolve: () => void; reject: (error: unknown) => void } + +type PendingRenewal = { request: ControlRenewalRequest; waiters: PendingWaiter[] } + +type QueuedRenewal = { request: ControlRenewalRequest; waiter: PendingWaiter } + +// Per host, not per activity: one statement updates a host's assignment row +// once, so two activities for the same host must not share a flush. +function pendingKey(request: ControlRenewalRequest): string { + return [request.identity.userId, request.identity.relayHostId].join('\u0000') +} + +// Collects the control-lease renewals a cell owes and spends one statement on +// them. Each caller still gets the single-renewal contract: the promise resolves +// on `renewed` and rejects with the outcome as its message otherwise, so callers +// keep their per-session error routing unchanged. +export class ControlRenewalBatch { + private pending = new Map() + // Renewals a host cannot contribute to the flush being built; they open the + // next one. + private deferred: QueuedRenewal[] = [] + private timer: ReturnType | null = null + + constructor( + private readonly renew: ( + rows: readonly ControlRenewalRequest[] + ) => Promise, + private readonly logFields: () => Record = () => ({}), + private readonly observe?: (flush: ControlRenewalFlush) => void + ) {} + + enqueue(request: ControlRenewalRequest): Promise { + return new Promise((resolve, reject) => { + this.admit({ request, waiter: { resolve, reject } }) + }) + } + + private admit(queued: QueuedRenewal): void { + const key = pendingKey(queued.request) + const existing = this.pending.get(key) + if (existing && existing.request.activityId !== queued.request.activityId) { + this.deferred.push(queued) + this.scheduleFlush() + return + } + if (existing) { + // A second attempt at the same lease inside one window supersedes the + // first expiry; both callers still hear the outcome they waited for. + existing.request = { + ...queued.request, + expiresAt: Math.max(existing.request.expiresAt, queued.request.expiresAt) + } + existing.waiters.push(queued.waiter) + return + } + this.pending.set(key, { request: queued.request, waiters: [queued.waiter] }) + if (this.pending.size >= CONTROL_RENEWAL_BATCH_MAX_ROWS) { + void this.flush() + return + } + this.scheduleFlush() + } + + private scheduleFlush(): void { + this.timer ??= setTimeout(() => { + this.timer = null + void this.flush() + }, CONTROL_RENEWAL_BATCH_INTERVAL_MS) + this.timer.unref?.() + } + + // Flushes run concurrently on purpose: a statement stalled in PostgreSQL must + // not hold back the renewals that came due while it was waiting. + async flush(): Promise { + if (this.timer) { + clearTimeout(this.timer) + this.timer = null + } + const batch = [...this.pending.values()] + this.pending = new Map() + // Re-admitted against the empty map, so a host deferred out of this flush + // leads the next one. + const deferred = this.deferred + this.deferred = [] + for (const queued of deferred) this.admit(queued) + if (batch.length === 0) return + const startedAt = performance.now() + let outcomes: ControlRenewalOutcome[] + try { + outcomes = await this.renew(batch.map((entry) => entry.request)) + } catch (error) { + for (const entry of batch) for (const waiter of entry.waiters) waiter.reject(error) + this.report(batch.length, performance.now() - startedAt, { flush_failed: batch.length }) + return + } + const counts: Record = {} + for (const [index, entry] of batch.entries()) { + const outcome = outcomes[index] ?? 'database_error' + counts[outcome] = (counts[outcome] ?? 0) + 1 + for (const waiter of entry.waiters) { + if (outcome === 'renewed') waiter.resolve() + else waiter.reject(new Error(outcome)) + } + } + this.report(batch.length, performance.now() - startedAt, counts) + } + + private report(rows: number, durationMs: number, outcomes: Record): void { + this.observe?.({ rows, durationMs, outcomes }) + const renewed = outcomes.renewed ?? 0 + if (durationMs <= CONTROL_RENEWAL_SLOW_FLUSH_MS && renewed === rows) return + console.warn( + JSON.stringify({ + event: 'orca_relay_control_renewal_flush', + ...this.logFields(), + rows, + durationMs: Math.round(durationMs), + outcomes + }) + ) + } +} diff --git a/cloud/apps/relay/src/control-renewal-postgres.test.ts b/cloud/apps/relay/src/control-renewal-postgres.test.ts index fb49e3af3a2..354c9edf44a 100644 --- a/cloud/apps/relay/src/control-renewal-postgres.test.ts +++ b/cloud/apps/relay/src/control-renewal-postgres.test.ts @@ -1,8 +1,11 @@ +import { performance } from 'node:perf_hooks' import { ASSIGNMENT_LIMITS, RELAY_PROTOCOL_LIMITS } from '@orca-cloud/relay-contract' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { RelayAssignmentStore } from './assignment-store.js' +import { CONTROL_RENEWAL_BATCH_SQL } from './control-renewal-statement.js' import { openRelayDatabase, + POSTGRES_LOCK_TIMEOUT_MS, type RelayDatabase, type RelayLockOptions, type SqlRow @@ -22,7 +25,9 @@ const targetCell = { capacityRequests: 100 } const userId = 'control-renewal-postgres-user' -const identities = Array.from({ length: 6 }, (_, index) => ({ +// Indexes 0-5 belong to the single-renewal cases below, which mutate their +// host's migration and lease state; the batch cases own 6-13. +const identities = Array.from({ length: 14 }, (_, index) => ({ userId, relayHostId: `controlrenewal${index + 1}` })) @@ -72,7 +77,7 @@ class StallFirstRenewalQueryDatabase implements RelayDatabase { constructor(private readonly database: RelayDatabase) {} async query(sql: string, params?: unknown[]): Promise { - if (this.stallNext && sql.includes('WITH assignment_state AS MATERIALIZED')) { + if (this.stallNext && sql === CONTROL_RENEWAL_BATCH_SQL) { this.stallNext = false this.stalled.resolve() await this.continue.promise @@ -103,7 +108,7 @@ class RenewalQueryProbeDatabase implements RelayDatabase { constructor(private readonly database: RelayDatabase) {} async query(sql: string, params?: unknown[]): Promise { - if (sql.includes('WITH assignment_state AS MATERIALIZED')) this.renewalQueries++ + if (sql === CONTROL_RENEWAL_BATCH_SQL) this.renewalQueries++ return await this.database.query(sql, params) } @@ -332,6 +337,164 @@ describePostgres('PostgreSQL control renewal', () => { ).rejects.toThrow('invalid_activity_expiry') }) + it('renews every due host in one autocommitted statement', async () => { + const probe = new RenewalQueryProbeDatabase(database) + const store = new RelayAssignmentStore(probe, () => now) + const batch = identities.slice(6, 10) + now += 30_000 + const expiresAt = now + 105_000 + + const outcomes = await store.renewControlActivities( + batch.map((identity) => ({ + identity, + activityId: controlId(sourceCell.id), + cellId: sourceCell.id, + expiresAt + })) + ) + + expect(outcomes).toEqual(['renewed', 'renewed', 'renewed', 'renewed']) + expect(probe.renewalQueries).toBe(1) + expect(probe.transactions).toBe(0) + const leases = await database.query( + `SELECT relay_host_id, expires_at FROM relay_assignment_activity_leases + WHERE user_id = ? AND activity_id = ? ORDER BY relay_host_id ASC`, + [userId, controlId(sourceCell.id)] + ) + expect( + leases + .filter((lease) => + batch.some((identity) => identity.relayHostId === lease.relay_host_id) + ) + .map((lease) => Number(lease.expires_at)) + ).toEqual([expiresAt, expiresAt, expiresAt, expiresAt]) + }) + + it('reports each host its own verdict inside one batch', async () => { + const store = new RelayAssignmentStore(database, () => now) + now += 30_000 + const expiresAt = now + 105_000 + const live = identities[10]! + const absent = { userId, relayHostId: 'controlrenewalgone' } + + const outcomes = await store.renewControlActivities([ + { identity: absent, activityId: controlId(sourceCell.id), cellId: sourceCell.id, expiresAt }, + { identity: live, activityId: controlId(sourceCell.id), cellId: sourceCell.id, expiresAt }, + { + identity: live, + activityId: controlId(targetCell.id), + cellId: targetCell.id, + expiresAt + } + ]) + + expect(outcomes).toEqual([ + 'assignment_not_found', + 'renewed', + 'activity_cell_not_authoritative' + ]) + }) + + it('passes over a host whose assignment row is held and renews the rest', async () => { + const store = new RelayAssignmentStore(database, () => now) + now += 30_000 + const expiresAt = now + 105_000 + const held = identities[11]! + const free = identities[12]! + const locked = signal() + const release = signal() + // Holds the row the way every per-host transactional path does. + const holder = database.transaction(async (transaction) => { + await transaction.queryLocked( + `SELECT * FROM relay_assignments WHERE user_id = ? AND relay_host_id = ?`, + [held.userId, held.relayHostId] + ) + locked.resolve() + await release.promise + }) + await locked.promise + + const startedAt = performance.now() + const outcomes = await store.renewControlActivities( + [held, free].map((identity) => ({ + identity, + activityId: controlId(sourceCell.id), + cellId: sourceCell.id, + expiresAt + })) + ) + const elapsedMs = performance.now() - startedAt + release.resolve() + await holder + + expect(outcomes).toEqual(['assignment_lock_unavailable', 'renewed']) + // It skipped rather than queued: a blocking FOR UPDATE would have spent the + // pool's whole lock_timeout here and failed the free host too. + expect(elapsedMs).toBeLessThan(POSTGRES_LOCK_TIMEOUT_MS) + const lease = ( + await database.query( + `SELECT expires_at FROM relay_assignment_activity_leases + WHERE user_id = ? AND relay_host_id = ? AND activity_id = ?`, + [free.userId, free.relayHostId, controlId(sourceCell.id)] + ) + )[0] + expect(Number(lease!.expires_at)).toBe(expiresAt) + }) + + it('renews both of one host\u2019s control leases in a single batch', async () => { + const store = new RelayAssignmentStore(database, () => now) + const identity = identities[13]! + // Two live control leases on one host. Written directly because + // activateControl retires the prior generation, and what is under test is the + // statement's row-wise behaviour, not how the second lease came to exist. + await database.query( + `INSERT INTO relay_assignment_activity_leases + (user_id, relay_host_id, activity_id, activity_kind, cell_id, + request_units, expires_at, updated_at) + VALUES (?, ?, ?, 'control', ?, 1, ?, ?)`, + [ + identity.userId, + identity.relayHostId, + `control:${sourceCell.id}:2`, + sourceCell.id, + now, + now + ] + ) + now += 30_000 + const expiresAt = now + 105_000 + + const outcomes = await store.renewControlActivities( + [1, 2].map((generation) => ({ + identity, + activityId: `control:${sourceCell.id}:${generation}`, + cellId: sourceCell.id, + expiresAt: expiresAt - generation + })) + ) + + expect(outcomes).toEqual(['renewed', 'renewed']) + const leases = await database.query( + `SELECT activity_id, expires_at FROM relay_assignment_activity_leases + WHERE user_id = ? AND relay_host_id = ? ORDER BY activity_id ASC`, + [identity.userId, identity.relayHostId] + ) + expect(leases.map((lease) => Number(lease.expires_at))).toEqual([ + expiresAt - 1, + expiresAt - 2 + ]) + // The assignment row is written once, carrying the later of the two. + const row = ( + await database.query( + `SELECT lease_expires_at, last_activity_at FROM relay_assignments + WHERE user_id = ? AND relay_host_id = ?`, + [identity.userId, identity.relayHostId] + ) + )[0] + expect(Number(row!.lease_expires_at)).toBe(expiresAt - 1) + expect(Number(row!.last_activity_at)).toBe(now) + }) + it('uses one autocommitted PostgreSQL statement for a steady renewal', async () => { const probe = new RenewalQueryProbeDatabase(database) const store = new RelayAssignmentStore(probe, () => now) diff --git a/cloud/apps/relay/src/control-renewal-statement.ts b/cloud/apps/relay/src/control-renewal-statement.ts new file mode 100644 index 00000000000..486f825c10d --- /dev/null +++ b/cloud/apps/relay/src/control-renewal-statement.ts @@ -0,0 +1,230 @@ +import type { AssignmentIdentity } from './assignment-identity-queue.js' +import type { SqlRow } from './database.js' + +export type ControlRenewalOutcome = + | 'renewed' + | 'assignment_not_found' + | 'activity_cell_not_authoritative' + | 'control_activity_not_found' + | 'control_activity_moved' + // The host's assignment row was already locked by one of the per-host + // transactional paths. Retryable, and never a reason to close a control: the + // next tick is 15s away and the lease has 105s on it. + | 'assignment_lock_unavailable' + // Decided per row before the statement runs, so one malformed request cannot + // cost the rest of the batch its renewal. + | 'invalid_activity_id' + | 'invalid_activity_expiry' + | 'database_error' + +// Outcomes the statement itself can report. `database_error` is raised by the +// driver, and `invalid_activity_expiry` is decided per row before the statement +// is built, so neither can come back as a row. +export const CONTROL_RENEWAL_STATEMENT_OUTCOMES = new Set([ + 'renewed', + 'assignment_not_found', + 'activity_cell_not_authoritative', + 'control_activity_not_found', + 'control_activity_moved', + 'assignment_lock_unavailable' +]) + +export type ControlRenewalRequest = { + identity: AssignmentIdentity + activityId: string + cellId: string + expiresAt: number +} + +// LOCK ORDER - (user_id, relay_host_id), the primary key of relay_assignments, +// applied here and repeated as the statement's ORDER BY so it holds whether the +// planner walks the primary-key index or sorts under the LockRows node. +// +// The batch never waits for an assignment row: SKIP LOCKED reports a contended +// host separately instead. That is what bounds how long a flush holds its locks +// to its own execution time, because row locks live until the statement commits, +// and it is why one host wedged in a per-host transaction cannot stall the +// renewals of every other host sharing the flush. +// +// With no wait on the assignment pass, the deadlock question reduces to the two +// later passes. Every writer in this store locks a host's assignment row before +// that host's lease rows (`assignmentRow` then `lockAssignmentActivities`), and +// a host whose assignment row is held was skipped, so the batch never reaches +// that host's lease: the lease pass cannot wait either. +// `markMigrationTargetRegistered` is the one writer that locks a migration row +// without the assignment row first. It takes no further locks, so it can delay a +// mid-migration row by up to the pool's lock_timeout but cannot close a cycle. +export function orderedControlRenewalRows( + rows: readonly Row[] +): Row[] { + return [...rows].sort( + (left, right) => + left.identity.userId.localeCompare(right.identity.userId) || + left.identity.relayHostId.localeCompare(right.identity.relayHostId) + ) +} + +// One statement renewing every due control lease on this cell, row-wise over the +// unnested parameter arrays. Logic per row is what the single-row predecessor +// did: lock the assignment, admit the caller's cell either as the current cell or +// as the source of an active forward migration, lock that host's control lease, +// push both expiries forward, and report one outcome. The one addition is +// `present_assignment`, an unlocked probe that separates a host with no +// assignment row at all from one whose row SKIP LOCKED passed over - the first +// closes the control, the second retries. +export const CONTROL_RENEWAL_BATCH_SQL = `WITH renewal_input AS MATERIALIZED ( + SELECT + renewal.ordinality AS row_index, + renewal.user_id, + renewal.relay_host_id, + renewal.activity_id, + renewal.cell_id, + renewal.expires_at + FROM unnest(?::text[], ?::text[], ?::text[], ?::text[], ?::bigint[]) + WITH ORDINALITY AS renewal( + user_id, relay_host_id, activity_id, cell_id, expires_at, ordinality + ) + ), present_assignment AS MATERIALIZED ( + SELECT input.row_index + FROM renewal_input input + JOIN relay_assignments assignment + ON assignment.user_id = input.user_id + AND assignment.relay_host_id = input.relay_host_id + ), assignment_state AS MATERIALIZED ( + SELECT input.row_index, assignment.cell_id, assignment.assignment_epoch + FROM renewal_input input + JOIN relay_assignments assignment + ON assignment.user_id = input.user_id + AND assignment.relay_host_id = input.relay_host_id + ORDER BY assignment.user_id, assignment.relay_host_id + FOR UPDATE OF assignment SKIP LOCKED + ), migration_state AS MATERIALIZED ( + SELECT locked.row_index + FROM assignment_state locked + JOIN renewal_input input ON input.row_index = locked.row_index + JOIN relay_assignment_migrations migration + ON migration.user_id = input.user_id + AND migration.relay_host_id = input.relay_host_id + AND migration.source_cell_id = input.cell_id + AND migration.target_cell_id = locked.cell_id + AND migration.assignment_epoch = locked.assignment_epoch + AND migration.completed_at IS NULL AND migration.aborted_at IS NULL + ORDER BY migration.user_id, migration.relay_host_id + FOR UPDATE OF migration + ), authorization_state AS MATERIALIZED ( + SELECT locked.row_index + FROM assignment_state locked + JOIN renewal_input input ON input.row_index = locked.row_index + WHERE locked.cell_id = input.cell_id + OR EXISTS ( + SELECT 1 FROM migration_state moving + WHERE moving.row_index = locked.row_index + ) + ), lease_state AS MATERIALIZED ( + SELECT authorized.row_index, lease.activity_kind, lease.cell_id + FROM authorization_state authorized + JOIN renewal_input input ON input.row_index = authorized.row_index + JOIN relay_assignment_activity_leases lease + ON lease.user_id = input.user_id + AND lease.relay_host_id = input.relay_host_id + AND lease.activity_id = input.activity_id + ORDER BY lease.user_id, lease.relay_host_id, lease.activity_id + FOR UPDATE OF lease + ), renewed_lease AS ( + UPDATE relay_assignment_activity_leases lease + SET expires_at = GREATEST(lease.expires_at, input.expires_at), + updated_at = GREATEST(lease.updated_at, ?) + FROM lease_state state + JOIN renewal_input input ON input.row_index = state.row_index + WHERE lease.user_id = input.user_id + AND lease.relay_host_id = input.relay_host_id + AND lease.activity_id = input.activity_id + AND state.activity_kind = 'control' AND state.cell_id = input.cell_id + RETURNING state.row_index + ), renewed_assignment AS ( + -- Grouped per host: an UPDATE whose FROM offers a target row more than + -- once applies one source row and returns one, so two leases on one + -- host would leave the assignment carrying the wrong expiry. The + -- aggregate hands it exactly one row, carrying the later expiry. + UPDATE relay_assignments assignment + SET lease_expires_at = GREATEST(assignment.lease_expires_at, renewed.expires_at), + last_activity_at = GREATEST(assignment.last_activity_at, ?) + FROM ( + SELECT input.user_id, input.relay_host_id, MAX(input.expires_at) AS expires_at + FROM renewed_lease renewed + JOIN renewal_input input ON input.row_index = renewed.row_index + GROUP BY input.user_id, input.relay_host_id + ) renewed + WHERE assignment.user_id = renewed.user_id + AND assignment.relay_host_id = renewed.relay_host_id + RETURNING renewed.user_id + ) + SELECT input.row_index, CASE + WHEN NOT EXISTS ( + SELECT 1 FROM present_assignment present + WHERE present.row_index = input.row_index + ) THEN 'assignment_not_found' + WHEN NOT EXISTS ( + SELECT 1 FROM assignment_state locked WHERE locked.row_index = input.row_index + ) THEN 'assignment_lock_unavailable' + WHEN NOT EXISTS ( + SELECT 1 FROM authorization_state authorized + WHERE authorized.row_index = input.row_index + ) THEN 'activity_cell_not_authoritative' + WHEN NOT EXISTS ( + SELECT 1 FROM lease_state state WHERE state.row_index = input.row_index + ) THEN 'control_activity_not_found' + WHEN EXISTS ( + SELECT 1 FROM lease_state state + WHERE state.row_index = input.row_index + AND (state.activity_kind <> 'control' OR state.cell_id <> input.cell_id) + ) THEN 'control_activity_moved' + -- Read from renewed_lease, which has one row per input row. The + -- assignment update collapses to one row per host, so it cannot answer + -- for a host that brought two leases to the same batch. + WHEN EXISTS ( + SELECT 1 FROM renewed_lease renewed + WHERE renewed.row_index = input.row_index + ) THEN 'renewed' + ELSE 'control_activity_not_found' + END AS outcome + FROM renewal_input input + ORDER BY input.row_index` + +export function controlRenewalBatchParams( + rows: readonly ControlRenewalRequest[], + now: number +): unknown[] { + return [ + rows.map((row) => row.identity.userId), + rows.map((row) => row.identity.relayHostId), + rows.map((row) => row.activityId), + rows.map((row) => row.cellId), + rows.map((row) => row.expiresAt), + now, + now + ] +} + +// Rows come back ordered by row_index, which is the 1-based position in the +// statement's parameter arrays. +export function readControlRenewalOutcomes( + rows: SqlRow[], + expected: number +): ControlRenewalOutcome[] { + if (rows.length !== expected) throw new Error('missing_control_renewal_outcome') + return rows.map((row, position) => { + if (Number(row.row_index) !== position + 1) { + throw new Error('misordered_control_renewal_outcome') + } + const outcome = row.outcome + if ( + typeof outcome !== 'string' || + !CONTROL_RENEWAL_STATEMENT_OUTCOMES.has(outcome as ControlRenewalOutcome) + ) { + throw new Error('invalid_control_renewal_outcome') + } + // SAFETY: the membership check above is what narrows this string. + return outcome as ControlRenewalOutcome + }) +} diff --git a/cloud/apps/relay/src/credential-cleanup-sweep-postgres.test.ts b/cloud/apps/relay/src/credential-cleanup-sweep-postgres.test.ts new file mode 100644 index 00000000000..45f38326d3a --- /dev/null +++ b/cloud/apps/relay/src/credential-cleanup-sweep-postgres.test.ts @@ -0,0 +1,283 @@ +import pg from 'pg' +import { afterAll, beforeEach, describe, expect, it } from 'vitest' +import { RelayCredentialStore, type RelayIdentity } from './credential-store.js' +import { openRelayDatabase, type RelayDatabase } from './database.js' + +// The outage this guards against: the credential cleanup ran every 30s in all 23 cells and both +// sweeps over relay_invites had no usable index, so each one seq-scanned the whole table inside the +// maintenance transaction. Only a real planner can show the partial indexes take that away, and +// only a real server has ctid. +const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL +const describePostgres = databaseUrl ? describe : describe.skip +const schema = 'relay_credential_sweep_test' + +const identity: RelayIdentity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' } +const DAY_MS = 24 * 60 * 60 * 1000 +const NOW = 100 * DAY_MS + +function scopedUrl(): string { + const url = new URL(databaseUrl!) + url.searchParams.set('options', `-c search_path=${schema}`) + return url.toString() +} + +async function onAdmin(operation: (client: pg.Client) => Promise): Promise { + const client = new pg.Client({ connectionString: databaseUrl }) + await client.connect() + try { + return await operation(client) + } finally { + await client.end() + } +} + +describePostgres('credential cleanup against PostgreSQL', () => { + let database: RelayDatabase + let store: RelayCredentialStore + const opened: RelayDatabase[] = [] + + beforeEach(async () => { + await onAdmin(async (client) => { + await client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`) + await client.query(`CREATE SCHEMA ${schema}`) + }) + database = await openRelayDatabase({ databaseUrl: scopedUrl(), dataDir: '' }) + opened.push(database) + store = new RelayCredentialStore(database, () => NOW) + }) + + afterAll(async () => { + await Promise.all(opened.map((open) => open.close().catch(() => undefined))) + await onAdmin((client) => client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`)) + }) + + async function seedInvites( + count: number, + state: string, + updatedAt: number, + expiresAt = NOW - DAY_MS + ): Promise { + await database.query( + `INSERT INTO relay_invites + (user_id, relay_host_id, relay_device_id, token_hash, state, attempt_count, + max_attempts, expires_at, created_at, updated_at) + SELECT ?, ?, 'device-' || n, 'token-' || ? || '-' || n, ?, 0, 3, ?, ?, ? + FROM generate_series(1, ?) AS n`, + [identity.userId, identity.relayHostId, state, state, expiresAt, updatedAt, updatedAt, count] + ) + } + + // Not through RelayDatabase: it routes anything that is not a SELECT to the row-count path, and + // EXPLAIN on an UPDATE is neither. + async function plan(sql: string, params: unknown[]): Promise { + const client = new pg.Client({ connectionString: scopedUrl() }) + await client.connect() + try { + let index = 0 + const result = await client.query( + `EXPLAIN ${sql.replace(/\?/g, () => `$${(index += 1)}`)}`, + params + ) + return result.rows.map((row) => String(row['QUERY PLAN'])).join('\n') + } finally { + await client.end() + } + } + + it('plans both invite sweeps as index scans instead of scanning the whole table', async () => { + // Production's shape: terminal invites outnumber live ones by orders of magnitude, which is + // what makes the partial predicates worth having. + await seedInvites(20_000, 'consumed', NOW) + for (const state of ['available', 'reserved', 'cooldown']) { + await seedInvites(200, state, NOW, NOW + DAY_MS) + } + await database.query( + `UPDATE relay_invites SET reservation_expires_at = ? WHERE state = 'reserved'`, + [NOW + 1] + ) + // Only ANALYZE makes the planner's row estimates real; without it a cold table looks tiny and + // a seq scan wins on any index. + await database.query(`ANALYZE relay_invites`) + + const expiry = await plan( + `UPDATE relay_invites SET state = 'expired' + WHERE expires_at <= ? AND state IN ('available', 'reserved', 'cooldown')`, + [NOW] + ) + const reservation = await plan( + `UPDATE relay_invites SET state = 'cooldown' + WHERE state = 'reserved' AND reservation_expires_at <= ? AND expires_at > ?`, + [NOW, NOW] + ) + + // Which of the two partial indexes serves the reservation pass is the planner's call: both + // predicates hold only live invites, so either one reads a handful of rows. The invariant is + // that neither pass reads the whole table any more. + for (const sweep of [expiry, reservation]) { + expect(sweep).not.toContain('Seq Scan on relay_invites') + expect(sweep).toMatch(/using relay_invites_sweep_(expiry|reservation)/) + } + expect(expiry).toContain('relay_invites_sweep_expiry') + }) + + it('plans the basis sweep off the composite index rather than the 1.5 GB heap', async () => { + // The shape that made this the most expensive statement in the sweep: 20,000 settled bases to + // 50 live ones. A partial index on active = 1 looks like the answer to that ratio and is not: + // a basis is inserted active and flipped to 0, so it accumulates the same dead entries, and + // the planner picks the composite index anyway. See the schema comment beside it. + await database.query( + `INSERT INTO relay_connection_bases + (basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + credential_kind, deadline, active, created_at) + SELECT 'settled-' || n, ?, ?, 'device-1', 1, 'invite', ?, 0, ? + FROM generate_series(1, 20000) AS n`, + [identity.userId, identity.relayHostId, NOW - 2 * DAY_MS, NOW - 2 * DAY_MS] + ) + await database.query( + `INSERT INTO relay_connection_bases + (basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + credential_kind, deadline, active, created_at) + SELECT 'live-' || n, ?, ?, 'device-1', 1, 'invite', ?, 1, ? + FROM generate_series(1, 50) AS n`, + [identity.userId, identity.relayHostId, NOW + 30_000, NOW] + ) + await database.query(`ANALYZE relay_connection_bases`) + + const sweep = await plan( + `UPDATE relay_connection_bases SET active = 0 WHERE active = 1 AND deadline <= ?`, + [NOW] + ) + + expect(sweep).not.toContain('Seq Scan on relay_connection_bases') + expect(sweep).toContain('using relay_connection_bases_active_deadline') + }) + + it('plans the drained basis reaper off the composite index, not the heap', async () => { + // Why the composite index stays for now: it is the only one covering active = 0, and the case + // that needs it is the steady state, where every row is inside retention and the reaper must + // learn there is nothing to do. While the backlog drains the planner rightly prefers a bounded + // sequential scan, because it finds its 5,000 rows and stops; measured at 200k rows, the + // drained batch costs 5 buffers with this index and 1,274 without it. + await database.query( + `INSERT INTO relay_connection_bases + (basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + credential_kind, deadline, active, created_at) + SELECT 'settled-' || n, ?, ?, 'device-1', 1, 'invite', ?, 0, ? + FROM generate_series(1, 20000) AS n`, + [identity.userId, identity.relayHostId, NOW - 60_000, NOW - 60_000] + ) + await database.query(`ANALYZE relay_connection_bases`) + + const reaper = await plan( + `DELETE FROM relay_connection_bases WHERE ctid IN ( + SELECT ctid FROM relay_connection_bases WHERE active = ? AND deadline <= ? LIMIT 5000 + )`, + [0, NOW - DAY_MS] + ) + + expect(reaper).toContain('relay_connection_bases_active_deadline') + expect(reaper).not.toContain('Seq Scan on relay_connection_bases') + }) + + it('plans the pending-authorization and rate-window sweeps as index scans', async () => { + await database.query( + `INSERT INTO relay_direct_authorizations + (direct_auth_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + deadline, consumed_at) + SELECT 'auth-' || n, ?, ?, 'device-1', 1, ?, ? + FROM generate_series(1, 20000) AS n`, + [identity.userId, identity.relayHostId, NOW - 1, NOW - 1] + ) + await database.query( + `INSERT INTO relay_rate_windows (scope_key, window_kind, window_started_at, count) + SELECT 'scope-' || n, 'invite-mint', ?, 1 FROM generate_series(1, 20000) AS n`, + [NOW] + ) + await database.query(`ANALYZE relay_direct_authorizations`) + await database.query(`ANALYZE relay_rate_windows`) + + const pending = await plan( + `UPDATE relay_direct_authorizations SET consumed_at = ? + WHERE consumed_at IS NULL AND deadline <= ?`, + [NOW, NOW] + ) + const windows = await plan(`DELETE FROM relay_rate_windows WHERE window_started_at < ?`, [ + NOW - DAY_MS + ]) + + expect(pending).toContain('relay_direct_authorizations_pending_deadline') + expect(pending).not.toContain('Seq Scan on relay_direct_authorizations') + expect(windows).toContain('relay_rate_windows_started') + expect(windows).not.toContain('Seq Scan on relay_rate_windows') + }) + + it('reaps settled bases and consumed authorizations through ctid', async () => { + await database.query( + `INSERT INTO relay_connection_bases + (basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + credential_kind, deadline, active, created_at) + SELECT 'settled-' || n, ?, ?, 'device-1', 1, 'invite', ?, 0, ? + FROM generate_series(1, 5002) AS n`, + [identity.userId, identity.relayHostId, NOW - 2 * DAY_MS, NOW - 2 * DAY_MS] + ) + await database.query( + `INSERT INTO relay_connection_bases + (basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + credential_kind, deadline, active, created_at) + VALUES ('live', ?, ?, 'device-1', 1, 'invite', ?, 1, ?)`, + [identity.userId, identity.relayHostId, NOW + 30_000, NOW] + ) + await database.query( + `INSERT INTO relay_direct_authorizations + (direct_auth_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + deadline, consumed_at) + SELECT 'consumed-' || n, ?, ?, 'device-1', 1, ?, ? + FROM generate_series(1, 5002) AS n`, + [identity.userId, identity.relayHostId, NOW - 2 * DAY_MS, NOW - 2 * DAY_MS] + ) + await database.query( + `INSERT INTO relay_direct_authorizations + (direct_auth_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + deadline, consumed_at) + VALUES ('pending', ?, ?, 'device-1', 1, ?, NULL)`, + [identity.userId, identity.relayHostId, NOW + 30_000] + ) + + await store.cleanup() + expect( + await database.query(`SELECT count(*) AS total FROM relay_connection_bases`) + ).toEqual([{ total: '3' }]) + expect( + await database.query(`SELECT count(*) AS total FROM relay_direct_authorizations`) + ).toEqual([{ total: '3' }]) + + await store.cleanup() + // Only the rows a reader could still accept are left. + expect( + await database.query(`SELECT basis_conn_id FROM relay_connection_bases`) + ).toEqual([{ basis_conn_id: 'live' }]) + expect( + await database.query(`SELECT direct_auth_id FROM relay_direct_authorizations`) + ).toEqual([{ direct_auth_id: 'pending' }]) + }) + + it('reaps terminal invites past retention through ctid, one bounded batch per cycle', async () => { + await seedInvites(5_002, 'consumed', NOW - 30 * DAY_MS) + await seedInvites(3, 'invalidated', NOW - 6 * DAY_MS) + await seedInvites(2, 'available', NOW - 400 * DAY_MS, NOW + DAY_MS) + + await store.cleanup() + expect(await database.query(`SELECT count(*) AS total FROM relay_invites`)).toEqual([ + { total: '7' } + ]) + + await store.cleanup() + // The two live invites and the three inside retention survive; the batch remainder is gone. + expect( + await database.query(`SELECT state, count(*) AS total FROM relay_invites GROUP BY state ORDER BY state`) + ).toEqual([ + { state: 'available', total: '2' }, + { state: 'invalidated', total: '3' } + ]) + }) +}) diff --git a/cloud/apps/relay/src/credential-store-cleanup.test.ts b/cloud/apps/relay/src/credential-store-cleanup.test.ts new file mode 100644 index 00000000000..ba9e7be7475 --- /dev/null +++ b/cloud/apps/relay/src/credential-store-cleanup.test.ts @@ -0,0 +1,297 @@ +import { describe, expect, it } from 'vitest' +import { RelayCredentialStore, type RelayIdentity } from './credential-store.js' +import { openInMemoryRelayDatabase, type RelayDatabase } from './database.js' + +const identity: RelayIdentity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' } +const DAY_MS = 24 * 60 * 60 * 1000 +const NOW = 100 * DAY_MS + +async function insertInvite( + database: RelayDatabase, + invite: { token: string; state: string; updatedAt: number; expiresAt?: number } +): Promise { + await database.query( + `INSERT INTO relay_invites + (user_id, relay_host_id, relay_device_id, token_hash, state, attempt_count, + max_attempts, expires_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + identity.userId, + identity.relayHostId, + `device-${invite.token}`, + invite.token, + invite.state, + 0, + 3, + invite.expiresAt ?? NOW + DAY_MS, + invite.updatedAt, + invite.updatedAt + ] + ) +} + +async function remainingTokens(database: RelayDatabase): Promise { + const rows = await database.query(`SELECT token_hash FROM relay_invites ORDER BY token_hash`) + return rows.map((row) => String(row.token_hash)) +} + +async function insertBasis( + database: RelayDatabase, + basis: { id: string; active: number; deadline: number } +): Promise { + await database.query( + `INSERT INTO relay_connection_bases + (basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + credential_kind, deadline, active, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + basis.id, + identity.userId, + identity.relayHostId, + 'device-1', + 1, + 'invite', + basis.deadline, + basis.active, + NOW + ] + ) +} + +async function insertDirectAuthorization( + database: RelayDatabase, + auth: { id: string; deadline: number; consumedAt: number | null } +): Promise { + await database.query( + `INSERT INTO relay_direct_authorizations + (direct_auth_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + deadline, consumed_at) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [auth.id, identity.userId, identity.relayHostId, 'device-1', 1, auth.deadline, auth.consumedAt] + ) +} + +async function remainingIds(database: RelayDatabase, table: string, column: string): Promise { + const rows = await database.query(`SELECT ${column} FROM ${table} ORDER BY ${column}`) + return rows.map((row) => String(row[column])) +} + +describe('credential cleanup invite reaper', () => { + it('deletes terminal invites past retention and keeps everything else', async () => { + const database = await openInMemoryRelayDatabase() + const store = new RelayCredentialStore(database, () => NOW) + const stale = NOW - 8 * DAY_MS + const recent = NOW - 6 * DAY_MS + for (const state of ['expired', 'consumed', 'invalidated']) { + await insertInvite(database, { token: `stale-${state}`, state, updatedAt: stale }) + await insertInvite(database, { token: `recent-${state}`, state, updatedAt: recent }) + } + + await store.cleanup() + + expect(await remainingTokens(database)).toEqual([ + 'recent-consumed', + 'recent-expired', + 'recent-invalidated' + ]) + await database.close() + }) + + it('never deletes an invite that a reader could still consume, however old', async () => { + // Retention is measured on updated_at, and a long-lived available invite has an old one. The + // state filter is what keeps the reaper from deleting a credential still in use. + const database = await openInMemoryRelayDatabase() + const store = new RelayCredentialStore(database, () => NOW) + const ancient = NOW - 400 * DAY_MS + for (const state of ['available', 'reserved', 'cooldown']) { + await insertInvite(database, { + token: `live-${state}`, + state, + updatedAt: ancient, + expiresAt: NOW + DAY_MS + }) + } + + await store.cleanup() + + expect(await remainingTokens(database)).toEqual(['live-available', 'live-cooldown', 'live-reserved']) + await database.close() + }) + + it('bounds one cycle to a single batch and drains the rest on later cycles', async () => { + const database = await openInMemoryRelayDatabase() + const store = new RelayCredentialStore(database, () => NOW) + const stale = NOW - 30 * DAY_MS + for (let index = 0; index < 5_002; index += 1) { + await insertInvite(database, { + token: `consumed-${String(index).padStart(5, '0')}`, + state: 'consumed', + updatedAt: stale + }) + } + + await store.cleanup() + expect(await remainingTokens(database)).toHaveLength(2) + + await store.cleanup() + expect(await remainingTokens(database)).toEqual([]) + await database.close() + }) + + it('still expires credentials the sweep owns, and only those past their deadline', async () => { + // The reaper runs after the sweep in the same call, so this pins that adding it did not + // displace any of the five state transitions the sweep is there for. + const database = await openInMemoryRelayDatabase() + const store = new RelayCredentialStore(database, () => NOW) + await insertInvite(database, { + token: 'lapsed', + state: 'available', + updatedAt: NOW, + expiresAt: NOW - 1 + }) + await insertInvite(database, { + token: 'current', + state: 'available', + updatedAt: NOW, + expiresAt: NOW + DAY_MS + }) + await database.query( + `UPDATE relay_invites SET state = ?, reservation_expires_at = ? WHERE token_hash = ?`, + ['reserved', NOW - 1, 'current'] + ) + await database.query( + `INSERT INTO relay_connection_bases + (basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation, + credential_kind, deadline, active, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + 'basis-lapsed', identity.userId, identity.relayHostId, 'device-1', 1, 'invite', NOW - 1, 1, NOW, + 'basis-live', identity.userId, identity.relayHostId, 'device-1', 1, 'invite', NOW + 1, 1, NOW + ] + ) + await store.recordDirectAuthorization({ + ...identity, + relayDeviceId: 'device-1', + directAuthId: 'direct-lapsed', + owningControlGeneration: 1, + deadline: NOW - 1 + }) + await store.recordDirectAuthorization({ + ...identity, + relayDeviceId: 'device-1', + directAuthId: 'direct-live', + owningControlGeneration: 1, + deadline: NOW + 1 + }) + await database.query( + `INSERT INTO relay_rate_windows (scope_key, window_kind, window_started_at, count) + VALUES (?, ?, ?, ?), (?, ?, ?, ?)`, + ['scope', 'invite-mint', NOW - 2 * DAY_MS, 1, 'scope', 'invite-mint', NOW - 1, 1] + ) + + await store.cleanup() + + expect( + await database.query(`SELECT token_hash, state FROM relay_invites ORDER BY token_hash`) + ).toEqual([ + { token_hash: 'current', state: 'cooldown' }, + { token_hash: 'lapsed', state: 'expired' } + ]) + expect( + await database.query(`SELECT basis_conn_id, active FROM relay_connection_bases ORDER BY basis_conn_id`) + ).toEqual([ + { basis_conn_id: 'basis-lapsed', active: 0 }, + { basis_conn_id: 'basis-live', active: 1 } + ]) + expect( + await database.query( + `SELECT direct_auth_id FROM relay_direct_authorizations + WHERE consumed_at IS NULL ORDER BY direct_auth_id` + ) + ).toEqual([{ direct_auth_id: 'direct-live' }]) + expect(await database.query(`SELECT window_started_at FROM relay_rate_windows`)).toEqual([ + { window_started_at: NOW - 1 } + ]) + await database.close() + }) + + it('reaps connection bases whose deadline passed over a day ago, and nothing else', async () => { + // Retention is measured on deadline, and both readers of a basis require deadline >= now, so a + // deadline a day in the past is already unusable however the active flag reads. The active = 0 + // clause is what keeps the batch an index range, not what makes the row safe to delete. + const database = await openInMemoryRelayDatabase() + const store = new RelayCredentialStore(database, () => NOW) + await insertBasis(database, { id: 'stale-inactive', active: 0, deadline: NOW - 2 * DAY_MS }) + await insertBasis(database, { id: 'recent-inactive', active: 0, deadline: NOW - 60_000 }) + // A long-lived splice: still active hours after the 30s deadline it was created with. The + // sweep deactivates it this cycle and the reaper takes it in the same call, which is safe + // precisely because no reader would have accepted it since its deadline passed. + await insertBasis(database, { id: 'stale-active', active: 1, deadline: NOW - 400 * DAY_MS }) + await insertBasis(database, { id: 'live-active', active: 1, deadline: NOW + DAY_MS }) + + await store.cleanup() + + expect(await remainingIds(database, 'relay_connection_bases', 'basis_conn_id')).toEqual([ + 'live-active', + 'recent-inactive' + ]) + // The one row a reader can still use is untouched, active flag included. + expect( + await database.query( + `SELECT active FROM relay_connection_bases WHERE basis_conn_id = 'live-active'` + ) + ).toEqual([{ active: 1 }]) + await database.close() + }) + + it('reaps consumed direct authorizations past retention and never a pending one', async () => { + const database = await openInMemoryRelayDatabase() + const store = new RelayCredentialStore(database, () => NOW) + await insertDirectAuthorization(database, { + id: 'stale-consumed', + deadline: NOW - 2 * DAY_MS, + consumedAt: NOW - 2 * DAY_MS + }) + await insertDirectAuthorization(database, { + id: 'recent-consumed', + deadline: NOW - 60_000, + consumedAt: NOW - 60_000 + }) + await insertDirectAuthorization(database, { + id: 'pending-ancient', + deadline: NOW + DAY_MS, + consumedAt: null + }) + + await store.cleanup() + + expect(await remainingIds(database, 'relay_direct_authorizations', 'direct_auth_id')).toEqual([ + 'pending-ancient', + 'recent-consumed' + ]) + await database.close() + }) + + it('bounds each table to one batch per cycle', async () => { + const database = await openInMemoryRelayDatabase() + const store = new RelayCredentialStore(database, () => NOW) + for (let index = 0; index < 5_001; index += 1) { + const id = String(index).padStart(5, '0') + await insertBasis(database, { id: `basis-${id}`, active: 0, deadline: NOW - 2 * DAY_MS }) + await insertDirectAuthorization(database, { + id: `auth-${id}`, + deadline: NOW - 2 * DAY_MS, + consumedAt: NOW - 2 * DAY_MS + }) + } + + await store.cleanup() + expect(await remainingIds(database, 'relay_connection_bases', 'basis_conn_id')).toHaveLength(1) + expect(await remainingIds(database, 'relay_direct_authorizations', 'direct_auth_id')).toHaveLength(1) + + await store.cleanup() + expect(await remainingIds(database, 'relay_connection_bases', 'basis_conn_id')).toEqual([]) + expect(await remainingIds(database, 'relay_direct_authorizations', 'direct_auth_id')).toEqual([]) + await database.close() + }) +}) diff --git a/cloud/apps/relay/src/credential-store.ts b/cloud/apps/relay/src/credential-store.ts index a5697e8c699..8d1281bb3c5 100644 --- a/cloud/apps/relay/src/credential-store.ts +++ b/cloud/apps/relay/src/credential-store.ts @@ -12,6 +12,17 @@ const CREDENTIAL_GRACE_MS = 24 * 60 * 60 * 1000 // tolerance at exactly inviteTtlMs; issuing under the ceiling keeps pairing // working for clients whose clocks trail the cell by up to this margin. const INVITE_ISSUE_SKEW_MARGIN_MS = 30 * 1000 +// Terminal invites are read by nothing: every reader re-checks expiry and state at read time, so +// the row only serves the audit trail, which relay_audit_events already keeps. A week is long +// enough to answer a support question about a pairing that failed. +const TERMINAL_INVITE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000 +// Why: both readers of a connection basis and of a direct authorization require it still +// active/unconsumed AND inside its deadline, and every deadline is set at most 30s past insert, so +// a settled row can never authorize anything again. A day is margin for forensics, not for reads. +const INACTIVE_AUTHORIZATION_RETENTION_MS = 24 * 60 * 60 * 1000 +// Bounded so one cycle cannot hold row locks or grow WAL without limit; the backlog drains over +// however many cycles it takes. +const REAP_BATCH_ROWS = 5000 export type RelayIdentity = { userId: string; relayHostId: string } export type CredentialReservation = RelayIdentity & { @@ -643,6 +654,44 @@ export class RelayCredentialStore { [now - 24 * 60 * 60 * 1000] ) }) + await this.reapSettledCredentials(now) + } + + // Outside the sweep transaction on purpose: each delete is idempotent and independent of the + // state transitions above, so batching them in would only hold their row locks for longer. + private async reapSettledCredentials(now: number): Promise { + await this.reapBatch( + 'relay_invites', + 'state IN (?, ?, ?) AND updated_at <= ?', + ['expired', 'consumed', 'invalidated', now - TERMINAL_INVITE_RETENTION_MS] + ) + // deadline, not created_at: it is the second column of relay_connection_bases_active_deadline, + // so once the backlog is drained this batch learns there is nothing left to do from the index + // instead of the 1.5 GB heap. Both readers reject a passed deadline, so a day past one is + // unusable whatever the active flag says. + await this.reapBatch('relay_connection_bases', 'active = ? AND deadline <= ?', [ + 0, + now - INACTIVE_AUTHORIZATION_RETENTION_MS + ]) + // consumed_at, not deadline: consumption is what settles this row, and it can happen well + // before the deadline, so measuring from it retains the row for the full window either way. + await this.reapBatch( + 'relay_direct_authorizations', + 'consumed_at IS NOT NULL AND consumed_at <= ?', + [now - INACTIVE_AUTHORIZATION_RETENTION_MS] + ) + } + + // ctid/rowid, not the primary key: the physical address lets the delete re-find exactly the batch + // the subquery located instead of re-matching the predicate per row. + private async reapBatch(table: string, predicate: string, params: unknown[]): Promise { + const address = this.database.dialect === 'sqlite' ? 'rowid' : 'ctid' + await this.database.query( + `DELETE FROM ${table} WHERE ${address} IN ( + SELECT ${address} FROM ${table} WHERE ${predicate} LIMIT ${REAP_BATCH_ROWS} + )`, + params + ) } private async installStatusWith( diff --git a/cloud/apps/relay/src/database-postgres-timeout.test.ts b/cloud/apps/relay/src/database-postgres-timeout.test.ts index aeb730496c9..adc27cc451c 100644 --- a/cloud/apps/relay/src/database-postgres-timeout.test.ts +++ b/cloud/apps/relay/src/database-postgres-timeout.test.ts @@ -134,7 +134,7 @@ describe('PostgreSQL relay deadlines', () => { statements.every( (statement) => statement === POSTGRES_STATEMENT_STATS_MIGRATION.trim() || - /^(?:CREATE|ALTER TABLE)\b/i.test(body(statement)) + /^(?:CREATE|ALTER TABLE|DROP INDEX)\b/i.test(body(statement)) ) ).toBe(true) // The backfill is DML, so it stays on the deadline-bearing serving pool. diff --git a/cloud/apps/relay/src/database.ts b/cloud/apps/relay/src/database.ts index 53f1836296e..972f3ce2aa1 100644 --- a/cloud/apps/relay/src/database.ts +++ b/cloud/apps/relay/src/database.ts @@ -100,6 +100,18 @@ CREATE TABLE IF NOT EXISTS relay_invites ( CREATE INDEX IF NOT EXISTS relay_invites_device ON relay_invites(user_id, relay_host_id, relay_device_id); +-- schema-deferrable: created out of band, so a boot that cannot take the lock must retry +-- Why: the credential sweep matches (state, expires_at) every cycle while invites in a terminal +-- state accumulate for the life of the database. Unindexed it seq-scans the whole table inside the +-- maintenance transaction. Partial, so the index holds only the states the sweep can act on. +CREATE INDEX IF NOT EXISTS relay_invites_sweep_expiry + ON relay_invites(expires_at) WHERE state IN ('available', 'reserved', 'cooldown'); + +-- schema-deferrable: created out of band, so a boot that cannot take the lock must retry +-- Why: the second sweep pass matches (state, reservation_expires_at) over the same table. +CREATE INDEX IF NOT EXISTS relay_invites_sweep_reservation + ON relay_invites(reservation_expires_at) WHERE state = 'reserved'; + CREATE TABLE IF NOT EXISTS relay_devices ( user_id TEXT NOT NULL, relay_host_id TEXT NOT NULL, @@ -160,6 +172,11 @@ CREATE TABLE IF NOT EXISTS relay_connection_bases ( -- accumulate unboundedly. Unindexed it seq-scans millions of rows every cycle -- and holds the maintenance transaction open long enough to time out -- assignment lock waits. +-- Why not a partial index on active = 1: a basis is inserted active and flipped to 0, so each +-- deactivation leaves a dead entry in that index too. Measured on production-shaped history it +-- carries the same dead entries as this one, the planner picks this one in every state, and it +-- costs ~65 bytes of WAL per insert. Bloat here is cured by reaping and vacuum, not by a narrower +-- index. CREATE INDEX IF NOT EXISTS relay_connection_bases_active_deadline ON relay_connection_bases(active, deadline); @@ -173,6 +190,12 @@ CREATE TABLE IF NOT EXISTS relay_direct_authorizations ( consumed_at BIGINT ); +-- schema-deferrable: created out of band, so a boot that cannot take the lock must retry +-- Why: the sweep expires pending authorizations by (consumed_at IS NULL, deadline), and consumed +-- rows are never deleted. Partial, so the index stays the size of the pending set. +CREATE INDEX IF NOT EXISTS relay_direct_authorizations_pending_deadline + ON relay_direct_authorizations(deadline) WHERE consumed_at IS NULL; + CREATE TABLE IF NOT EXISTS relay_confirm_results ( user_id TEXT NOT NULL, relay_host_id TEXT NOT NULL, @@ -524,8 +547,9 @@ CREATE TABLE IF NOT EXISTS relay_assignment_activity_leases ( updated_at BIGINT NOT NULL, PRIMARY KEY (user_id, relay_host_id, activity_id) ); -CREATE INDEX IF NOT EXISTS relay_assignment_activity_expiry - ON relay_assignment_activity_leases(expires_at); +-- expires_at is deliberately unindexed: every control renewal writes it (~471/s), so an index on +-- it makes each renewal a non-HOT update that rewrites index entries. Its only reader is the 30s +-- expiry sweep, which seq-scans 14.8k rows / 7MB in a few milliseconds. CREATE TABLE IF NOT EXISTS relay_control_connection_reservations ( reservation_id TEXT PRIMARY KEY, @@ -559,6 +583,12 @@ CREATE TABLE IF NOT EXISTS relay_rate_windows ( PRIMARY KEY (scope_key, window_kind, window_started_at) ); +-- schema-deferrable: created out of band, so a boot that cannot take the lock must retry +-- Why: window_started_at is the PRIMARY KEY's last column, so the sweep's 24h retention delete +-- cannot use it and seq-scans instead. +CREATE INDEX IF NOT EXISTS relay_rate_windows_started + ON relay_rate_windows(window_started_at); + CREATE TABLE IF NOT EXISTS relay_migration_leases ( user_id TEXT NOT NULL, relay_host_id TEXT NOT NULL, @@ -645,7 +675,24 @@ export const POSTGRES_SCHEMA_MIGRATIONS = [ ADD COLUMN IF NOT EXISTS host_cooldown_ms BIGINT NOT NULL DEFAULT ${REGIONAL_REHOME_DEFAULT_HOST_COOLDOWN_MS}`, `ALTER TABLE relay_control_capabilities ADD COLUMN IF NOT EXISTS idle_regional_rehome BIGINT NOT NULL DEFAULT 0`, - `ALTER TABLE relay_region_rehome_attempts ADD COLUMN IF NOT EXISTS source_generation BIGINT NOT NULL DEFAULT 0` + `ALTER TABLE relay_region_rehome_attempts ADD COLUMN IF NOT EXISTS source_generation BIGINT NOT NULL DEFAULT 0`, + // Dropped, not created: see the comment on relay_assignment_activity_leases. Deferrable because + // this is the one boot where it has to take ACCESS EXCLUSIVE on a table under continuous write, + // and all 28 directors reach it at once; a lock timeout here must not restart the instance, which + // would only re-queue the same DDL behind the same writers. Once it wins, the pre-check answers + // absent and no later boot sends it at all. + `-- schema-deferrable: one boot has to win ACCESS EXCLUSIVE on a table written ~475/s + DROP INDEX IF EXISTS relay_assignment_activity_expiry`, + // The drop is what makes HOT legal; this is what makes it possible. A renewal can only reuse the + // row's own page when that page has room for a second version, and at the default fillfactor of + // 100 a freshly filled page has none - measured at 0.5% HOT with the index gone and the default, + // against 100% at 70. Takes SHARE UPDATE EXCLUSIVE, which blocks vacuum and DDL but no reader or + // writer, and only for the catalog write. Applies to pages as they refill, so the table converges + // over its own renewal cycle rather than at boot. + // Deferrable for the same reason, though SHARE UPDATE EXCLUSIVE blocks only vacuum and DDL: it + // buys nothing until the drop lands, so a boot that deferred the drop should defer this too. + `-- schema-deferrable: buys nothing until the drop above lands + ALTER TABLE relay_assignment_activity_leases SET (fillfactor = 70)` ] // The exact statement list a Postgres boot applies, in order, so the lock-target census can read diff --git a/cloud/apps/relay/src/host-session-client-accept.test.ts b/cloud/apps/relay/src/host-session-client-accept.test.ts index 0b7febaa7b4..7806e8d25b3 100644 --- a/cloud/apps/relay/src/host-session-client-accept.test.ts +++ b/cloud/apps/relay/src/host-session-client-accept.test.ts @@ -729,3 +729,73 @@ describe('control lease jitter', () => { vi.advanceTimersByTime(0) }) }) + +describe('paced drain and the phones of a host not yet told', () => { + beforeEach(() => vi.useFakeTimers()) + afterEach(() => { + vi.clearAllTimers() + vi.useRealTimers() + }) + + const laterHostId = 'qrstuvwxyz012345' + const laterIdentity = { ...identity, sub: 'user-2', relayHostId: laterHostId } + + async function twoHostCell(): Promise<{ + h: ReturnType + told: FakeSocket + untold: FakeSocket + }> { + const h = harness() + const told = await activeHost(h) + const untold = new FakeSocket() + await h.activate(untold as unknown as WebSocket, laterIdentity, null, 1, false, 1, '1.4.197') + // Both hosts now dial in, so the credential mocks have to answer for either. + h.store.resolveResume.mockImplementation(async (hostId: string) => ({ + userId: hostId === laterHostId ? laterIdentity.sub : identity.sub + })) + h.store.reserveCredential.mockImplementation(async (hostId: string) => ({ + ...reservation, + userId: hostId === laterHostId ? laterIdentity.sub : identity.sub, + relayHostId: hostId + })) + return { h, told, untold } + } + + async function dial(h: ReturnType, hostId: string): Promise { + const client = new FakeSocket() + await h.registry.acceptClient(client as unknown as WebSocket, hostId, 'credential') + return client + } + + it('serves a host whose drain has not been sent and refuses one whose has', async () => { + const { h, told, untold } = await twoHostCell() + h.registry.drain(0, { paceWindowMs: 40_000 }) + + const refused = await dial(h, identity.relayHostId) + expect(refused.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.DRAINING, expect.any(String)) + expect(told.send).not.toHaveBeenCalledWith(expect.stringContaining('conn-open')) + + const served = await dial(h, laterHostId) + expect(served.close).not.toHaveBeenCalled() + expect(untold.send).toHaveBeenCalledWith(expect.stringContaining('conn-open')) + }) + + it('refuses that host\'s phones as soon as its own drain is sent', async () => { + const { h, untold } = await twoHostCell() + h.registry.drain(0, { paceWindowMs: 40_000 }) + await vi.advanceTimersByTimeAsync(40_000) + expect(untold.send).toHaveBeenCalledWith(expect.stringContaining('"type":"drain"')) + + const refused = await dial(h, laterHostId) + expect(refused.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.DRAINING, expect.any(String)) + }) + + it('keeps an unpaced drain refusing every phone at once', async () => { + const { h } = await twoHostCell() + h.registry.drain(0) + for (const hostId of [identity.relayHostId, laterHostId]) { + const refused = await dial(h, hostId) + expect(refused.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.DRAINING, expect.any(String)) + } + }) +}) diff --git a/cloud/apps/relay/src/host-session-registry.test.ts b/cloud/apps/relay/src/host-session-registry.test.ts index 7c50285454c..69697cce60e 100644 --- a/cloud/apps/relay/src/host-session-registry.test.ts +++ b/cloud/apps/relay/src/host-session-registry.test.ts @@ -12,6 +12,12 @@ import type WebSocket from 'ws' import type { RelayAssignmentStore } from './assignment-store.js' import type { RelayConfig } from './config.js' import type { RelayCredentialStore } from './credential-store.js' +import { CONTROL_RENEWAL_BATCH_INTERVAL_MS } from './control-renewal-batch.js' +import { + CONTROL_RENEWAL_STATEMENT_OUTCOMES, + type ControlRenewalOutcome, + type ControlRenewalRequest +} from './control-renewal-statement.js' import { HostSessionRegistry, type HostSession } from './host-session-registry.js' import { relayHostLogDigest } from './relay-host-log-digest.js' import type { RelayRuntimeObserver } from './relay-observability.js' @@ -23,6 +29,12 @@ import { import type { RelayTokenClaims } from './relay-token-verifier.js' import { ProcessQueuedByteBudget } from './splice-forwarder.js' +// A due renewal leaves the heartbeat as a batch enqueue, so the store only sees +// the tick once the batch window closes. +async function closeRenewalWindow(): Promise { + await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) +} + class FakeSocket extends EventEmitter { readonly OPEN = 1 readonly CLOSING = 2 @@ -109,6 +121,7 @@ function createRegistry( activate: ActivateSession acquireActivity: ReturnType renewControlActivity: ReturnType + renewControlActivities: ReturnType releaseActivity: ReturnType observer: { recordAuth: ReturnType @@ -119,12 +132,38 @@ function createRegistry( const acquireActivity = vi.fn().mockResolvedValue(undefined) const renewControlActivity = vi.fn().mockResolvedValue(undefined) const releaseActivity = vi.fn().mockResolvedValue(true) + // Mirrors the store's own batch semantics over the single-renewal mock: a known + // outcome becomes that row's verdict, and any other failure reaches the caller + // as the driver's error. Keeps every per-call expectation below aimed at the + // renewal a session actually asked for. + const renewControlActivities = vi.fn( + async (rows: readonly ControlRenewalRequest[]): Promise => + await Promise.all( + rows.map(async (row): Promise => { + try { + await renewControlActivity(row.identity, { + activityId: row.activityId, + cellId: row.cellId, + expiresAt: row.expiresAt + }) + return 'renewed' + } catch (error) { + const message = String((error as { message?: unknown }).message) + if (!CONTROL_RENEWAL_STATEMENT_OUTCOMES.has(message as ControlRenewalOutcome)) { + throw error + } + return message as ControlRenewalOutcome + } + }) + ) + ) const assignments = { activateControl, markMigrationTargetRegistered: vi.fn().mockResolvedValue(undefined), resolve: vi.fn().mockResolvedValue({ cellId: config.cellId }), acquireActivity, renewControlActivity, + renewControlActivities, releaseActivity } as unknown as RelayAssignmentStore const observer = { @@ -176,6 +215,7 @@ function createRegistry( activate, acquireActivity, renewControlActivity, + renewControlActivities, releaseActivity, observer } @@ -336,15 +376,15 @@ describe('host session cleanup races', () => { session.activeSplices.set('conn-a', () => session.activeSplices.delete('conn-a')) // POST /v1/admin/drain has no idempotency guard, and SIGTERM then SIGINT both - // reach drain(), so a second teardown can be scheduled for the same session. + // reach drain(), so a retry re-sends to every session. It must re-arm the pending + // teardown rather than stack a second one: across a paced cell that is 800 orphaned + // timers per retry, each one holding the loop open for the rest of the window. registry.drain(0) const scheduled = vi.getTimerCount() registry.drain(0) - // Pin the premise: if drain ever gains an idempotency guard, the retry schedules no - // second teardown and the assertion below stops defending the write-once snapshot - // while still passing. Compare against the count before the retry rather than an - // absolute, since the session's heartbeat interval is also pending. - expect(vi.getTimerCount()).toBe(scheduled + 1) + // Compare against the count before the retry rather than an absolute, since the + // session's heartbeat interval is also pending. + expect(vi.getTimerCount()).toBe(scheduled) vi.advanceTimersByTime(1) // Asserting registry state, not the log line: FakeSocket closes synchronously, so @@ -689,7 +729,8 @@ describe('host session cleanup races', () => { expect(original).not.toBeNull() await activate(new FakeSocket() as unknown as WebSocket, identity, original, 2, false, 1) - vi.advanceTimersByTime(15_000) + await vi.advanceTimersByTimeAsync(15_000) + await closeRenewalWindow() expect(renewControlActivity).toHaveBeenCalledOnce() expect(renewControlActivity).toHaveBeenCalledWith( @@ -715,6 +756,7 @@ describe('host session cleanup races', () => { }) ) await vi.advanceTimersByTimeAsync(15_000) + await closeRenewalWindow() const replacement = new FakeSocket() await h.activate(replacement as unknown as WebSocket, identity, session, 1, true, 1) reject(new Error('activity_cell_not_authoritative')) @@ -737,6 +779,7 @@ describe('host session cleanup races', () => { }) ) await vi.advanceTimersByTimeAsync(15_000) + await closeRenewalWindow() h.registry.drainHost({ attemptId: 'attempt', userId: identity.sub, @@ -762,6 +805,7 @@ describe('host session cleanup races', () => { for (let interval = 0; interval < 4; interval++) { await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) socket.emit('message', Buffer.from(JSON.stringify({ type: 'pong' })), false) + await closeRenewalWindow() } const pings = socket.send.mock.calls.filter((call) => String(call[0]).includes('"ping"')) @@ -791,8 +835,10 @@ describe('host session cleanup races', () => { try { await activate(socket as unknown as WebSocket, identity, null, 1, false, 1) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(renewControlActivity).toHaveBeenCalledOnce() await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(renewControlActivity).toHaveBeenCalledTimes(2) } finally { warn.mockRestore() @@ -812,6 +858,7 @@ describe('host session cleanup races', () => { await activate(socket as unknown as WebSocket, identity, null, 1, false, 1) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs * 2) + await closeRenewalWindow() expect(renewControlActivity).toHaveBeenCalledTimes(2) stalled.resolve(undefined) @@ -831,13 +878,16 @@ describe('host session cleanup races', () => { await activate(socket as unknown as WebSocket, identity, null, 1, false, 1) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs * 2) + await closeRenewalWindow() expect(renewControlActivity).toHaveBeenCalledTimes(2) stalled.resolve(undefined) await vi.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(renewControlActivity).toHaveBeenCalledTimes(2) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(renewControlActivity).toHaveBeenCalledTimes(3) registry.drain(0) @@ -855,6 +905,7 @@ describe('host session cleanup races', () => { await activate(socket as unknown as WebSocket, identity, null, 1, false, 1) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(acquireActivity).toHaveBeenCalledWith( { userId: identity.sub, relayHostId: identity.relayHostId }, @@ -880,6 +931,7 @@ describe('host session cleanup races', () => { await activate(socket as unknown as WebSocket, identity, null, 1, false, 1) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(acquireActivity).not.toHaveBeenCalled() expect(socket.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.DRAINING, 'control activity moved') @@ -899,6 +951,7 @@ describe('host session cleanup races', () => { await activate(socket as unknown as WebSocket, identity, null, 1, false, 1) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(socket.close).toHaveBeenCalledWith( RELAY_CLOSE_CODE.DRAINING, @@ -918,6 +971,7 @@ describe('host session cleanup races', () => { await activate(socket as unknown as WebSocket, identity, null, 1, false, 1) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(socket.close).toHaveBeenCalledWith( RELAY_CLOSE_CODE.DRAINING, @@ -939,6 +993,7 @@ describe('host session cleanup races', () => { for (let interval = 0; interval < 3; interval++) { await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) socket.emit('message', Buffer.from(JSON.stringify({ type: 'pong' })), false) + await closeRenewalWindow() } expect(renewControlActivity).toHaveBeenCalledTimes(2) @@ -966,6 +1021,7 @@ describe('control renewal cadence across a rebind', () => { const beat = async (target: FakeSocket): Promise => { await vi.advanceTimersByTimeAsync(ping) target.emit('message', Buffer.from(JSON.stringify({ type: 'pong' })), false) + await closeRenewalWindow() } // Age the session so its attempt counter is well above zero. @@ -993,6 +1049,57 @@ describe('control renewal cadence across a rebind', () => { }) }) +describe('control renewals shared by one batch', () => { + beforeEach(() => vi.useFakeTimers()) + afterEach(() => { + vi.clearAllTimers() + vi.useRealTimers() + }) + + it('renews two due hosts in one call and leaves a stale one alone', async () => { + const activateControl = vi + .fn() + .mockResolvedValueOnce('control:production-gce-c3:1') + .mockResolvedValueOnce('control:production-gce-c3:1') + const { registry, activate, renewControlActivities } = createRegistry(activateControl) + const other = { ...identity, sub: 'user-2', relayHostId: 'ponmlkjihgfedcba' } + const staleSocket = new FakeSocket() + const liveSocket = new FakeSocket() + await activate(staleSocket as unknown as WebSocket, identity, null, 1, false, 1) + await activate(liveSocket as unknown as WebSocket, other, null, 1, false, 1) + const stale = registry.get({ userId: identity.sub, relayHostId: identity.relayHostId })! + const live = registry.get({ userId: other.sub, relayHostId: other.relayHostId })! + + // Both come due inside the same window, and one socket goes away while the + // statement is still in PostgreSQL. + let release!: () => void + renewControlActivities.mockImplementationOnce( + async (rows: readonly ControlRenewalRequest[]) => { + staleSocket.close() + await new Promise((resolve) => (release = resolve)) + return rows.map((): ControlRenewalOutcome => 'renewed') + } + ) + await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + const staleDueAt = stale.activityRenewalDueAt + await closeRenewalWindow() + release() + await vi.advanceTimersByTimeAsync(0) + + expect(renewControlActivities).toHaveBeenCalledOnce() + expect( + renewControlActivities.mock.calls[0]![0].map( + (row: ControlRenewalRequest) => row.identity.relayHostId + ) + ).toEqual([identity.relayHostId, other.relayHostId]) + expect(live.activityRenewalCompletedAttempt).toBe(1) + expect(stale.activityRenewalCompletedAttempt).toBe(0) + expect(stale.activityRenewalDueAt).toBe(staleDueAt) + registry.drain(0) + vi.advanceTimersByTime(0) + }) +}) + describe('control lease recovery after the session is gone', () => { beforeEach(() => vi.useFakeTimers()) afterEach(() => { @@ -1019,6 +1126,7 @@ describe('control lease recovery after the session is gone', () => { new Promise((_resolve, reject) => (failRenewal = reject)) ) await vi.advanceTimersByTimeAsync(RELAY_PROTOCOL_LIMITS.controlPingIntervalMs) + await closeRenewalWindow() expect(renewControlActivity).toHaveBeenCalledOnce() const newer = new FakeSocket() @@ -1696,3 +1804,124 @@ describe('host data attach owner lookup', () => { expect(h.owner.activeConnIds.size).toBe(0) }) }) + +describe('paced drain', () => { + beforeEach(() => vi.useFakeTimers()) + afterEach(() => { + vi.clearAllTimers() + vi.useRealTimers() + }) + + async function connectHosts(count: number): Promise<{ + registry: HostSessionRegistry + sockets: FakeSocket[] + }> { + const activateControl = vi + .fn() + .mockResolvedValue('control:production-gce-c3:1') + const { registry, activate } = createRegistry(activateControl) + const sockets: FakeSocket[] = [] + for (let index = 0; index < count; index += 1) { + const socket = new FakeSocket() + sockets.push(socket) + await activate( + socket as unknown as WebSocket, + { ...identity, sub: `user-${index}` }, + null, + 1, + false, + 1 + ) + socket.send.mockClear() + } + return { registry, sockets } + } + + function drainsSent(sockets: FakeSocket[]): number { + return sockets.filter((socket) => + socket.send.mock.calls.some(([payload]) => String(payload).includes('"type":"drain"')) + ).length + } + + it('sends every drain at once when no window is given', async () => { + const { registry, sockets } = await connectHosts(4) + registry.drain(0) + expect(drainsSent(sockets)).toBe(4) + }) + + // Windows here stay under the 75s control-silence watchdog, which would otherwise close + // a test socket that never heartbeats before its paced send is due. + it('spreads the sends evenly across the window', async () => { + const { registry, sockets } = await connectHosts(5) + registry.drain(0, { paceWindowMs: 40_000 }) + // The first host is sent synchronously; the last lands on the window's closing edge. + expect(drainsSent(sockets)).toBe(1) + await vi.advanceTimersByTimeAsync(10_000) + expect(drainsSent(sockets)).toBe(2) + await vi.advanceTimersByTimeAsync(20_000) + expect(drainsSent(sockets)).toBe(4) + await vi.advanceTimersByTimeAsync(10_000) + expect(drainsSent(sockets)).toBe(5) + }) + + it('fences admission for every session before the first paced send lands', async () => { + const { registry, sockets } = await connectHosts(3) + registry.drain(0, { paceWindowMs: 40_000 }) + expect(registry.isDraining()).toBe(true) + // A host whose drain has not been sent yet must already be non-authoritative. + const socket = new FakeSocket() + registry.acceptControl(socket as unknown as WebSocket, { ...identity, sub: 'user-late' }) + expect(socket.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.DRAINING, 'relay draining') + expect(drainsSent(sockets)).toBe(1) + }) + + it('gives each host its own grace after its own send, not after the call', async () => { + const { registry, sockets } = await connectHosts(2) + registry.drain(10_000, { paceWindowMs: 40_000 }) + await vi.advanceTimersByTimeAsync(10_000) + expect(sockets[0]!.readyState).toBe(sockets[0]!.CLOSED) + expect(sockets[1]!.readyState).toBe(sockets[1]!.OPEN) + // Its own send at 40s plus its own 10s grace, not 10s from the drain call. + await vi.advanceTimersByTimeAsync(39_999) + expect(sockets[1]!.readyState).toBe(sockets[1]!.OPEN) + await vi.advanceTimersByTimeAsync(10_001) + expect(sockets[1]!.readyState).toBe(sockets[1]!.CLOSED) + }) + + it('leaves no timer behind once an emergency drain cuts a window short', async () => { + const { registry } = await connectHosts(4) + registry.drain(0, { paceWindowMs: 40_000 }) + registry.drain(0) + await vi.advanceTimersByTimeAsync(0) + // Every session is closed, so anything still pending is an orphan of the cut window. + expect(vi.getTimerCount()).toBe(0) + }) + + it('keeps the first teardown snapshot when a regional drain fires before the fleet one', async () => { + const { registry, sockets } = await connectHosts(1) + const session = registry.get({ userId: 'user-0', relayHostId: identity.relayHostId })! + session.activeSplices.set('conn-a', () => session.activeSplices.delete('conn-a')) + registry.drainHost({ + attemptId: 'attempt', + userId: 'user-0', + relayHostId: identity.relayHostId, + sourceAssignmentEpoch: 1, + graceMs: 0 + }) + registry.drain(10) + await vi.advanceTimersByTimeAsync(11) + expect(session.closingCounts).toEqual({ splices: 1, pending: 0 }) + expect(sockets[0]!.readyState).toBe(sockets[0]!.CLOSED) + }) + + it('lets an emergency drain supersede the sends still queued by a paced one', async () => { + const { registry, sockets } = await connectHosts(4) + registry.drain(0, { paceWindowMs: 40_000 }) + expect(drainsSent(sockets)).toBe(1) + registry.drain(0) + expect(drainsSent(sockets)).toBe(4) + const sendsAfterEmergency = sockets.map((socket) => socket.send.mock.calls.length) + await vi.advanceTimersByTimeAsync(40_000) + expect(sockets.map((socket) => socket.send.mock.calls.length)).toEqual(sendsAfterEmergency) + }) +}) diff --git a/cloud/apps/relay/src/host-session-registry.ts b/cloud/apps/relay/src/host-session-registry.ts index 0d0cf4a7940..480b1b6b914 100644 --- a/cloud/apps/relay/src/host-session-registry.ts +++ b/cloud/apps/relay/src/host-session-registry.ts @@ -26,6 +26,7 @@ import type WebSocket from 'ws' import type { RawData } from 'ws' import type { RelayConfig } from './config.js' import type { RelayAssignmentStore } from './assignment-store.js' +import { ControlRenewalBatch } from './control-renewal-batch.js' import { RelayCredentialStore, type CredentialReservation } from './credential-store.js' import { HostCloseReasonMemory } from './host-close-reason-memory.js' import { relayHostLogDigest } from './relay-host-log-digest.js' @@ -179,6 +180,10 @@ export class HostSessionRegistry { private readonly hostCloseReasons = new HostCloseReasonMemory(() => this.now()) private readonly hostCapabilities = new WeakMap>() private draining = false + private readonly drainTimers = new Set>() + // Hosts whose drain has been sent. Paced sends land minutes apart, so "this cell is + // draining" is not the same question as "this host has been told to leave". + private readonly drainSentHosts = new Set() private readonly idleWork = new Map() private readonly idleAttempts = new Map< @@ -299,6 +304,15 @@ export class HostSessionRegistry { private readonly cellIncarnation?: string ) {} + // Renewals leave the heartbeat as an enqueue: one statement per cell per + // window replaces one write transaction per host, which is what keeps the + // shared PostgreSQL instance out of buffer-header contention. + private readonly controlRenewals = new ControlRenewalBatch( + async (rows) => await this.assignments.renewControlActivities(rows), + () => this.logIdentity(), + (flush) => this.observer.recordControlRenewalFlush?.(flush) + ) + // Uniform over [CONTROL_LEASE_MS - jitter, CONTROL_LEASE_MS + jitter). private controlLeaseExpiresAt(): number { const offset = Math.floor((this.random() * 2 - 1) * CONTROL_LEASE_JITTER_MS) @@ -330,7 +344,10 @@ export class HostSessionRegistry { credential: string, capacityReservation?: PendingHostDataReservation ): Promise { - if (this.draining) { + // Not `this.draining`: a paced drain tells hosts minutes apart, and the director keeps + // pointing phones here until their own host has moved. Refusing them for the whole + // window would turn a 2 min drain into a 2 min outage for hosts not yet told. + if (this.drainSentHosts.has(hostId)) { capacityReservation?.release() this.rejectClient(socket, RELAY_CLOSE_CODE.DRAINING) return @@ -450,7 +467,7 @@ export class HostSessionRegistry { } // Admission may have crossed a drain or control replacement while persisting activity. if ( - this.draining || + this.drainSentHosts.has(hostId) || this.sessions.get(sessionKey) !== session || session.state !== 'active' || session.socket !== admittingSocket || @@ -592,7 +609,7 @@ export class HostSessionRegistry { } // Already admitted attachments may finish a regional drain, but never a retired generation. if ( - this.draining || + this.drainSentHosts.has(identity.relayHostId) || this.sessions.get(this.key(identity.userId, identity.relayHostId)) !== session || this.get(identity)?.state === 'closed' || !session.activeConnIds.has(connId) || @@ -846,17 +863,49 @@ export class HostSessionRegistry { return { controls, splices, pendingSplices } } - drain(graceMs: number): void { + drain(graceMs: number, options: { paceWindowMs?: number } = {}): void { this.draining = true - for (const session of this.sessions.values()) { - if (session.state === 'closed') continue - session.authorityRevision += 1 - session.state = 'drain-only' - if (session.socket) send(session.socket, 'drain', { graceMs, recovery: 'resolve-director' }) - setTimeout(() => this.closeDrainedSession(session), graceMs) + // A later drain (an emergency one, or shutdown) owns every session again, so nothing + // queued by an earlier paced drain may still fire: it would re-send and, worse, keep + // the event loop alive for the rest of a window the operator just cut short. + for (const timer of this.drainTimers) clearTimeout(timer) + this.drainTimers.clear() + const paceWindowMs = Math.max(0, Math.trunc(options.paceWindowMs ?? 0)) + const targets = [...this.sessions.values()].filter((session) => session.state !== 'closed') + // The desktop re-dials the director as soon as it reads `drain`, whatever graceMs says, + // so spreading the send is the only thing that spreads the reconnect load. + const step = paceWindowMs > 0 && targets.length > 1 ? paceWindowMs / (targets.length - 1) : 0 + for (const [index, session] of targets.entries()) { + const delay = Math.round(step * index) + if (delay === 0) { + this.sendDrain(session, graceMs) + continue + } + this.scheduleDrainTimer(delay, () => this.sendDrain(session, graceMs)) } } + // A session is only fenced when it is told, not when the drain starts: until its send + // lands it is an ordinary live host, and its phones have to keep being able to reach it. + private sendDrain(session: HostSession, graceMs: number): void { + if (session.state === 'closed') return + session.authorityRevision += 1 + session.state = 'drain-only' + this.drainSentHosts.add(session.relayHostId) + if (session.socket) send(session.socket, 'drain', { graceMs, recovery: 'resolve-director' }) + this.scheduleDrainTimer(graceMs, () => this.closeDrainedSession(session)) + } + + // Unref'd so a drain in flight never holds the process open past its own work. + private scheduleDrainTimer(delayMs: number, run: () => void): void { + const timer: ReturnType = setTimeout(() => { + this.drainTimers.delete(timer) + run() + }, delayMs) + timer.unref?.() + this.drainTimers.add(timer) + } + drainHost(input: { attemptId: string userId: string @@ -1345,15 +1394,13 @@ export class HostSessionRegistry { session.controlActivityId === controlActivityId && session.authorityRevision === authorityRevision && attempt > session.activityRenewalCompletedAttempt - void this.assignments - .renewControlActivity( - { userId: session.identity.sub, relayHostId: session.relayHostId }, - { - activityId: controlActivityId, - cellId: this.config.cellId, - expiresAt: startedAt + CONTROL_ACTIVITY_LEASE_MS - } - ) + void this.controlRenewals + .enqueue({ + identity: { userId: session.identity.sub, relayHostId: session.relayHostId }, + activityId: controlActivityId, + cellId: this.config.cellId, + expiresAt: startedAt + CONTROL_ACTIVITY_LEASE_MS + }) .then(() => { if (!current()) return session.activityRenewalCompletedAttempt = attempt @@ -1419,6 +1466,13 @@ export class HostSessionRegistry { session.socket?.close(RELAY_CLOSE_CODE.DRAINING, 'control activity moved') return } + if (error instanceof Error && error.message === 'assignment_lock_unavailable') { + // A per-host transaction held the row, so the batch passed over it + // rather than making every other host in the flush wait. The next + // tick is 15s away against a 105s lease, and the flush line already + // reports the count, so this needs no line of its own. + return + } console.warn('[orca-relay] control activity renewal failed') }) // Terminal handler: a throw inside the async catch above (e.g. a diff --git a/cloud/apps/relay/src/index.ts b/cloud/apps/relay/src/index.ts index 8e7b6a56941..36a89b9de0a 100644 --- a/cloud/apps/relay/src/index.ts +++ b/cloud/apps/relay/src/index.ts @@ -46,14 +46,19 @@ const { ready, cellIncarnation } = createRelayServer(config, database) -const cleanupTimer = setInterval( - () => - void runRelayBackgroundOperation( - () => store.cleanup(), - '[orca-relay] credential cleanup failed' - ), - 30_000 -) +// Same owner as the assignment sweep: the cleanup only expires credentials that every reader +// already re-checks at read time, so running it in all 23 cells multiplied one table scan by 23 +// without changing any answer. +const cleanupTimer = roleOwnsAssignmentMaintenance(config.role) + ? setInterval( + () => + void runRelayBackgroundOperation( + () => store.cleanup(), + '[orca-relay] credential cleanup failed' + ), + jitteredSweepIntervalMs(30_000) + ) + : null const assignmentCleanupTimer = roleOwnsAssignmentMaintenance(config.role) ? setInterval(() => { void runAssignmentCleanup(assignments) @@ -82,7 +87,7 @@ const migrationInventoryTimer = roleOwnsAssignmentMaintenance(config.role) }, '[orca-relay] migration inventory failed') }, 5 * 60_000) : null -cleanupTimer.unref() +cleanupTimer?.unref() assignmentCleanupTimer?.unref() inventorySnapshotTimer?.unref() migrationInventoryTimer?.unref() @@ -128,7 +133,7 @@ server.listen(config.port, () => { }) const shutdown = (): void => { - clearInterval(cleanupTimer) + if (cleanupTimer) clearInterval(cleanupTimer) if (assignmentCleanupTimer) clearInterval(assignmentCleanupTimer) if (inventorySnapshotTimer) clearInterval(inventorySnapshotTimer) if (migrationInventoryTimer) clearInterval(migrationInventoryTimer) diff --git a/cloud/apps/relay/src/regional-host-drain-app.test.ts b/cloud/apps/relay/src/regional-host-drain-app.test.ts index cf7e3798155..38ebdd3f709 100644 --- a/cloud/apps/relay/src/regional-host-drain-app.test.ts +++ b/cloud/apps/relay/src/regional-host-drain-app.test.ts @@ -697,6 +697,78 @@ async function postPath( }) } +describe('cell drain endpoint pacing', () => { + function appWithDrain(): { + app: ReturnType + drain: ReturnType + } { + const drain = vi.fn() + const app = createRelayApp(config(), { + store: {} as never, + assignments: {} as never, + drain, + cellIncarnation, + ready: vi.fn(async () => true) + } as Parameters[1]) + return { app, drain } + } + + it('drains everything at once when the caller asks for no pacing', async () => { + const { app, drain } = appWithDrain() + const response = await postPath(app, '/v1/admin/drain', 'deploy-token', { v: 1, graceMs: 0 }) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ ok: true, paceWindowMs: 0 }) + expect(drain).toHaveBeenCalledWith(0, { paceWindowMs: 0 }) + }) + + it('passes the requested window through and echoes what it accepted', async () => { + const { app, drain } = appWithDrain() + const response = await postPath(app, '/v1/admin/drain', 'deploy-token', { + v: 1, + graceMs: 0, + paceWindowMs: 120_000 + }) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ ok: true, paceWindowMs: 120_000 }) + expect(drain).toHaveBeenCalledWith(0, { paceWindowMs: 120_000 }) + }) + + it('refuses a window that is negative, fractional, or past the cap', async () => { + for (const paceWindowMs of [-1, 1.5, 300_001]) { + const { app, drain } = appWithDrain() + const response = await postPath(app, '/v1/admin/drain', 'deploy-token', { + v: 1, + graceMs: 0, + paceWindowMs + }) + expect(response.status).toBe(400) + expect(drain).not.toHaveBeenCalled() + } + }) + + it('accepts the cap itself', async () => { + const { app, drain } = appWithDrain() + const response = await postPath(app, '/v1/admin/drain', 'deploy-token', { + v: 1, + graceMs: 0, + paceWindowMs: 300_000 + }) + expect(response.status).toBe(200) + expect(drain).toHaveBeenCalledWith(0, { paceWindowMs: 300_000 }) + }) + + it('still rejects an unauthenticated pacing request', async () => { + const { app, drain } = appWithDrain() + const response = await postPath(app, '/v1/admin/drain', 'wrong-token', { + v: 1, + graceMs: 0, + paceWindowMs: 120_000 + }) + expect(response.status).toBe(401) + expect(drain).not.toHaveBeenCalled() + }) +}) + function config(overrides: Partial = {}): RelayConfig { return { port: 8080, diff --git a/cloud/apps/relay/src/relay-observability.ts b/cloud/apps/relay/src/relay-observability.ts index 3ae1f4d3e93..dfd15eda928 100644 --- a/cloud/apps/relay/src/relay-observability.ts +++ b/cloud/apps/relay/src/relay-observability.ts @@ -1,6 +1,7 @@ import { monitorEventLoopDelay, performance } from 'node:perf_hooks' import { RELAY_REGION_METRIC_SEGMENTS, type RelayRegion } from '@orca-cloud/relay-contract' import type { ControlRenewalOutcome } from './assignment-store.js' +import type { ControlRenewalFlush } from './control-renewal-batch.js' import type { CellInventoryHoldCounts } from './cell-inventory-hold-samples.js' import type { PostgresPoolPressureCounts } from './postgres-pool-pressure.js' import type { RelayReadinessGraceEvent, RelayReadinessObservation } from './relay-readiness.js' @@ -53,6 +54,7 @@ export interface RelayRuntimeObserver { recordReconnect(): void recordSql(durationMs: number, success: boolean): void recordControlRenewal?(durationMs: number, outcome: ControlRenewalOutcome): void + recordControlRenewalFlush?(flush: ControlRenewalFlush): void recordControlActivityRecovery?(success: boolean): void recordAssignmentAdmission?(outcome: AssignmentAdmissionOutcome): void recordAssignmentRejectionReason?(lane: AssignmentAdmissionLane, reason: string): void @@ -119,6 +121,8 @@ type RelayMetricDeltas = { controlRttObserved: number controlRenewalLatenciesMs: number[] controlRenewalsByOutcome: Record + controlRenewalFlushLatenciesMs: number[] + controlRenewalFlushRowsMax: number controlActivityRecoveries: number controlActivityRecoveryFailures: number } @@ -164,6 +168,8 @@ const emptyDeltas = (): RelayMetricDeltas => ({ controlRttObserved: 0, controlRenewalLatenciesMs: [], controlRenewalsByOutcome: {}, + controlRenewalFlushLatenciesMs: [], + controlRenewalFlushRowsMax: 0, controlActivityRecoveries: 0, controlActivityRecoveryFailures: 0 }) @@ -274,6 +280,14 @@ export class RelayObservability implements RelayRuntimeObserver { (this.deltas.controlRenewalsByOutcome[outcome] ?? 0) + 1 } + recordControlRenewalFlush(flush: ControlRenewalFlush): void { + this.deltas.controlRenewalFlushLatenciesMs.push(flush.durationMs) + this.deltas.controlRenewalFlushRowsMax = Math.max( + this.deltas.controlRenewalFlushRowsMax, + flush.rows + ) + } + recordControlActivityRecovery(success: boolean): void { if (success) this.deltas.controlActivityRecoveries++ else this.deltas.controlActivityRecoveryFailures++ @@ -379,6 +393,7 @@ export class RelayObservability implements RelayRuntimeObserver { roundMs(percentile(deltas.clientAcceptStageSamplesMs[stage], 0.95)) const controlRtt = latencySummary(deltas.controlRttSamplesMs) const controlRenewal = latencySummary(deltas.controlRenewalLatenciesMs) + const controlRenewalFlush = latencySummary(deltas.controlRenewalFlushLatenciesMs) const memory = process.memoryUsage() const p99 = this.eventLoop.count === 0 ? 0 : this.eventLoop.percentile(99) / 1_000_000 this.eventLoop.reset() @@ -447,9 +462,16 @@ export class RelayObservability implements RelayRuntimeObserver { deltas.controlRenewalsByOutcome.control_activity_not_found ?? 0, controlActivityRecoveriesDelta: deltas.controlActivityRecoveries, controlActivityRecoveryFailuresDelta: deltas.controlActivityRecoveryFailures, + // Meaning changed when renewals began batching: for a batched row this is + // the flush's duration, not that row's own statement latency. The + // per-flush fields below are the ones to read for statement cost. controlRenewalLatencyMsP50: controlRenewal.p50, controlRenewalLatencyMsP95: controlRenewal.p95, controlRenewalLatencyMsMax: controlRenewal.max, + controlRenewalFlushesDelta: deltas.controlRenewalFlushLatenciesMs.length, + controlRenewalFlushRowsMax: deltas.controlRenewalFlushRowsMax, + controlRenewalFlushLatencyMsP95: controlRenewalFlush.p95, + controlRenewalFlushLatencyMsMax: controlRenewalFlush.max, httpLatencyMsMax: roundMs(deltas.httpLatencyMsMax), heapUsedBytes: memory.heapUsed, heapTotalBytes: memory.heapTotal, diff --git a/cloud/apps/relay/src/relay-schema-catalog-precheck-postgres.test.ts b/cloud/apps/relay/src/relay-schema-catalog-precheck-postgres.test.ts index 2ce9ad9dcb7..cdcc7b3d600 100644 --- a/cloud/apps/relay/src/relay-schema-catalog-precheck-postgres.test.ts +++ b/cloud/apps/relay/src/relay-schema-catalog-precheck-postgres.test.ts @@ -1,5 +1,5 @@ import pg from 'pg' -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { applyPostgresSchema, catalogObjectPresence, @@ -190,6 +190,79 @@ describePostgres('relay boot-time schema against PostgreSQL', () => { } }) + it('defers the activity-lease migrations and still boots while their table is locked', async () => { + // The migration boot, reproduced: the index is there, the table is locked by someone else, and + // all the drop can do is time out. It has to leave the statement for the next boot rather than + // fail, or 28 directors crash-loop through a stall on a table written ~475/s. + const cold = await openRelayDatabase({ databaseUrl: url, dataDir: '' }) + opened.push(cold) + // Put the database back in its pre-migration shape, which is what makes the drop lock-taking. + await pool.query( + `CREATE INDEX relay_assignment_activity_expiry + ON ${schema}.relay_assignment_activity_leases(expires_at)` + ) + await pool.query(`ALTER TABLE ${schema}.relay_assignment_activity_leases RESET (fillfactor)`) + + const warned: string[] = [] + const warn = vi.spyOn(console, 'warn').mockImplementation((line: string) => { + warned.push(line) + }) + const holder = new pg.Client({ connectionString: url }) + await holder.connect() + await holder.query('BEGIN') + await holder.query( + `LOCK TABLE ${schema}.relay_assignment_activity_leases IN ACCESS EXCLUSIVE MODE` + ) + let summary: Awaited> + try { + summary = await applyPostgresSchema( + relayPostgresSchemaStatements(), + (statement) => pool.query(statement), + { catalogQuery: async (sql, params) => (await pool.query(sql, params)).rows } + ) + } finally { + await holder.query('ROLLBACK') + await holder.end() + warn.mockRestore() + } + + // Both statements deferred, and the boot still applied everything else. + expect(summary.deferred).toBe(2) + expect(summary.ran).toBeGreaterThan(0) + const deferred = warned + .map((line) => JSON.parse(line) as { event?: string; name?: string }) + .filter((event) => event.event === 'orca_relay_postgres_schema_object_deferred') + expect(deferred.map((event) => event.name)).toEqual([ + 'relay_assignment_activity_expiry', + 'fillfactor=70' + ]) + // Nothing was applied, so the next boot has the same work to do, not half of it. + const stillThere = await pool.query( + `SELECT 1 FROM pg_indexes WHERE schemaname = $1 AND indexname = $2`, + [schema, 'relay_assignment_activity_expiry'] + ) + expect(stillThere.rowCount).toBe(1) + + // And the next boot, with the lock gone, finishes the job. + const retry = await applyPostgresSchema( + relayPostgresSchemaStatements(), + (statement) => pool.query(statement), + { catalogQuery: async (sql, params) => (await pool.query(sql, params)).rows } + ) + expect(retry.deferred).toBe(0) + const gone = await pool.query( + `SELECT 1 FROM pg_indexes WHERE schemaname = $1 AND indexname = $2`, + [schema, 'relay_assignment_activity_expiry'] + ) + expect(gone.rowCount).toBe(0) + const options = await pool.query( + `SELECT reloptions FROM pg_class WHERE oid = to_regclass($1)`, + [`${schema}.relay_assignment_activity_leases`] + ) + expect(options.rows[0]?.reloptions).toEqual(['fillfactor=70']) + await pool.end() + }) + it('fails that same boot with 55P03 when the pre-check is not wired in', async () => { // Keeps the test above from passing vacuously: the lock really does block relay's DDL. const cold = await openRelayDatabase({ databaseUrl: url, dataDir: '' }) diff --git a/cloud/apps/relay/src/relay-schema-lock-targets.test.ts b/cloud/apps/relay/src/relay-schema-lock-targets.test.ts index a490a394378..68a7c6f0216 100644 --- a/cloud/apps/relay/src/relay-schema-lock-targets.test.ts +++ b/cloud/apps/relay/src/relay-schema-lock-targets.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { requireSchemaLockTarget, + schemaDeferrable, schemaLockTarget, sqlWithoutComments, takesRelationLock, @@ -16,9 +17,22 @@ import { relayPostgresSchemaStatements } from './database.js' // CREATE INDEX CONCURRENTLY first, then add it to SCHEMA and update this list. const GOLDEN_LOCK_TAKING: SchemaLockTarget[] = [ { kind: 'index', table: 'relay_invites', name: 'relay_invites_device', skipWhen: 'present' }, + { kind: 'index', table: 'relay_invites', name: 'relay_invites_sweep_expiry', skipWhen: 'present' }, + { + kind: 'index', + table: 'relay_invites', + name: 'relay_invites_sweep_reservation', + skipWhen: 'present' + }, { kind: 'index', table: 'relay_devices', name: 'relay_devices_current_hash', skipWhen: 'present' }, { kind: 'index', table: 'relay_devices', name: 'relay_devices_grace_hash', skipWhen: 'present' }, { kind: 'index', table: 'relay_connection_bases', name: 'relay_connection_bases_active_deadline', skipWhen: 'present' }, + { + kind: 'index', + table: 'relay_direct_authorizations', + name: 'relay_direct_authorizations_pending_deadline', + skipWhen: 'present' + }, { kind: 'index', table: 'relay_assignment_region_preferences', @@ -72,12 +86,6 @@ const GOLDEN_LOCK_TAKING: SchemaLockTarget[] = [ name: 'relay_cell_drain_attempt_states_cell', skipWhen: 'present' }, - { - kind: 'index', - table: 'relay_assignment_activity_leases', - name: 'relay_assignment_activity_expiry', - skipWhen: 'present' - }, { kind: 'index', table: 'relay_control_connection_reservations', @@ -90,6 +98,7 @@ const GOLDEN_LOCK_TAKING: SchemaLockTarget[] = [ name: 'relay_control_connection_reservation_assignment', skipWhen: 'present' }, + { kind: 'index', table: 'relay_rate_windows', name: 'relay_rate_windows_started', skipWhen: 'present' }, { kind: 'index', table: 'relay_assignment_migrations', name: 'relay_assignment_migrations_active', skipWhen: 'present' }, { kind: 'index', @@ -116,7 +125,14 @@ const GOLDEN_LOCK_TAKING: SchemaLockTarget[] = [ }, { kind: 'column', table: 'relay_region_rehome_control', name: 'host_cooldown_ms', skipWhen: 'present' }, { kind: 'column', table: 'relay_control_capabilities', name: 'idle_regional_rehome', skipWhen: 'present' }, - { kind: 'column', table: 'relay_region_rehome_attempts', name: 'source_generation', skipWhen: 'present' } + { kind: 'column', table: 'relay_region_rehome_attempts', name: 'source_generation', skipWhen: 'present' }, + { kind: 'index-by-name', name: 'relay_assignment_activity_expiry', skipWhen: 'absent' }, + { + kind: 'reloption', + table: 'relay_assignment_activity_leases', + name: 'fillfactor=70', + skipWhen: 'present' + } ] const INDEX_OR_ADD_COLUMN = /^(?:CREATE\s+(?:UNIQUE\s+)?INDEX|ALTER\s+TABLE\s+[^\s]+\s+ADD\s+COLUMN)/i @@ -149,8 +165,12 @@ describe('relay boot-time lock targets', () => { for (const statement of relayPostgresSchemaStatements()) { const target = schemaLockTarget(statement) if (!target) continue - expect(target.name).toMatch(/^[a-z_][a-z0-9_]*$/) - expect(target.table).toMatch(/^[a-z_][a-z0-9_]*$/) + // A reloption is the one target whose name is a pair rather than an identifier, because + // pg_class stores reloptions as `name=value` text and the value is half the question. + const shape = target.kind === 'reloption' ? /^[a-z_][a-z0-9_]*=[A-Za-z0-9_.]+$/ : /^[a-z_][a-z0-9_]*$/ + expect(target.name).toMatch(shape) + // A DROP INDEX names no table, so there is none to check. + if (target.kind !== 'index-by-name') expect(target.table).toMatch(/^[a-z_][a-z0-9_]*$/) } }) @@ -194,7 +214,76 @@ describe('relay boot-time lock targets', () => { it('leaves every statement classifiable once its leading comments are stripped', () => { for (const statement of relayPostgresSchemaStatements()) { - expect(sqlWithoutComments(statement)).toMatch(/^(?:CREATE|ALTER|DO)\s/i) + expect(sqlWithoutComments(statement)).toMatch(/^(?:CREATE|ALTER|DROP|DO)\s/i) } }) + + it('pre-checks the activity-expiry drop by name, and skips it once the index is gone', () => { + // A DROP INDEX takes ACCESS EXCLUSIVE on the index's table for as long as the index is there, + // so it is in the census like any other lock-taking statement. Its target resolves by name + // alone, because the statement names no table and needs none. + const drops = relayPostgresSchemaStatements().filter((statement) => + /^DROP\s/i.test(sqlWithoutComments(statement)) + ) + expect(drops.map(sqlWithoutComments)).toEqual([ + 'DROP INDEX IF EXISTS relay_assignment_activity_expiry' + ]) + for (const statement of drops) { + expect(takesRelationLock(statement)).toBe(true) + expect(schemaLockTarget(statement)).toEqual({ + kind: 'index-by-name', + name: 'relay_assignment_activity_expiry', + skipWhen: 'absent' + }) + } + }) + + it('marks the out-of-band sweep indexes and the activity-lease migrations deferrable, and nothing else', () => { + // The statements a lock timeout must not turn into a crash loop, and the only ones: every + // other statement still fails the boot loudly, which is what keeps the marker meaningful. + const deferrable = relayPostgresSchemaStatements().filter(schemaDeferrable) + expect(deferrable.map((statement) => sqlWithoutComments(statement).replace(/\s+/g, ' '))).toEqual([ + "CREATE INDEX IF NOT EXISTS relay_invites_sweep_expiry ON relay_invites(expires_at) WHERE state IN ('available', 'reserved', 'cooldown')", + "CREATE INDEX IF NOT EXISTS relay_invites_sweep_reservation ON relay_invites(reservation_expires_at) WHERE state = 'reserved'", + 'CREATE INDEX IF NOT EXISTS relay_direct_authorizations_pending_deadline ON relay_direct_authorizations(deadline) WHERE consumed_at IS NULL', + 'CREATE INDEX IF NOT EXISTS relay_rate_windows_started ON relay_rate_windows(window_started_at)', + 'DROP INDEX IF EXISTS relay_assignment_activity_expiry', + 'ALTER TABLE relay_assignment_activity_leases SET (fillfactor = 70)' + ]) + }) + + it('derives a target for a partial index, WHERE clause and all', () => { + // The pre-check reads the index name and table from the head of the statement, so a trailing + // WHERE is invisible to it. Asserted because the sweep indexes depend on that: a parser that + // gave a partial index no target would send it unchecked on every boot. + const partial = relayPostgresSchemaStatements().filter((statement) => + /^CREATE\s+INDEX\b[\s\S]*\bWHERE\b/i.test(sqlWithoutComments(statement)) + ) + expect(partial.map(schemaLockTarget)).toEqual([ + { kind: 'index', table: 'relay_invites', name: 'relay_invites_sweep_expiry', skipWhen: 'present' }, + { + kind: 'index', + table: 'relay_invites', + name: 'relay_invites_sweep_reservation', + skipWhen: 'present' + }, + { + kind: 'index', + table: 'relay_direct_authorizations', + name: 'relay_direct_authorizations_pending_deadline', + skipWhen: 'present' + } + ]) + }) + + it('no longer creates an index on the column every control renewal writes', () => { + // The regression this drop exists to prevent: re-adding it would make ~471 renewals/s non-HOT + // again. A CREATE anywhere in the schema naming that index fails here. + const creates = relayPostgresSchemaStatements().filter((statement) => + /relay_assignment_activity_expiry/i.test(sqlWithoutComments(statement)) + ) + expect(creates.map(sqlWithoutComments)).toEqual([ + 'DROP INDEX IF EXISTS relay_assignment_activity_expiry' + ]) + }) }) diff --git a/cloud/apps/relay/src/relay-server.ts b/cloud/apps/relay/src/relay-server.ts index 50077b41116..3a0668bbcef 100644 --- a/cloud/apps/relay/src/relay-server.ts +++ b/cloud/apps/relay/src/relay-server.ts @@ -136,7 +136,7 @@ export function createRelayServer( const app = createRelayApp(config, { store, assignments, - drain: (graceMs) => sessions.drain(graceMs), + drain: (graceMs, options) => sessions.drain(graceMs, options ?? {}), drainHost: (input) => sessions.drainHost(input), idleRehome: (input) => { const now = (options.now ?? Date.now)() diff --git a/cloud/apps/relay/src/relay-sweep-schedule.test.ts b/cloud/apps/relay/src/relay-sweep-schedule.test.ts index b9965ae29ff..79f65a92582 100644 --- a/cloud/apps/relay/src/relay-sweep-schedule.test.ts +++ b/cloud/apps/relay/src/relay-sweep-schedule.test.ts @@ -52,4 +52,23 @@ describe('sweep schedule jitter', () => { expect(cleanup?.[1]).toBe('jitteredSweepIntervalMs(30_000)') }) + + it('jitters the credential cleanup tick', () => { + const source = readFileSync(new URL('./index.ts', import.meta.url), 'utf8') + const cleanup = /'\[orca-relay\] credential cleanup failed'\s*\),\s*([^\n]*?)\n/.exec(source) + + expect(cleanup?.[1]).toBe('jitteredSweepIntervalMs(30_000)') + }) + + // A census, not a list of the timers that happen to be gated today: an ungated sweep runs in + // every cell as well as the director, which multiplies one table scan by the fleet size. + it('gates every periodic sweep in index.ts on the maintenance role', () => { + const source = readFileSync(new URL('./index.ts', import.meta.url), 'utf8') + const timers = source.match(/setInterval\(/g) ?? [] + const gated = + source.match(/roleOwnsAssignmentMaintenance\(config\.role\)\s*\?\s*setInterval\(/g) ?? [] + + expect(timers.length).toBeGreaterThan(0) + expect(gated.length).toBe(timers.length) + }) }) diff --git a/cloud/dev/scripts/prepare-relay-production-capacity-canary.mjs b/cloud/dev/scripts/prepare-relay-production-capacity-canary.mjs index 7967d164e4b..9787dbdd62a 100644 --- a/cloud/dev/scripts/prepare-relay-production-capacity-canary.mjs +++ b/cloud/dev/scripts/prepare-relay-production-capacity-canary.mjs @@ -35,6 +35,9 @@ function cellOrigin(cellId) { // The same-cap roll covers the Asia cells the US-only capacity rollout never touches. const APPROVED_CELL_LISTS = { 'same-cap': SAME_CAP_CELLS } +// Matches the cell's own cap on /v1/admin/drain. +const MAX_PACE_WINDOW_MS = 5 * 60 * 1_000 + export function parseProductionCapacityCellArguments(argv) { const values = {} for (let index = 0; index < argv.length; index += 2) { @@ -64,11 +67,22 @@ export function parseProductionCapacityCellArguments(argv) { ) { throw new Error('production capacity target origin is not exact') } + const paceWindowMs = values['pace-window-ms'] === undefined + ? 0 + : Number(values['pace-window-ms']) + if ( + !Number.isSafeInteger(paceWindowMs) || + paceWindowMs < 0 || + paceWindowMs > MAX_PACE_WINDOW_MS + ) { + throw new Error('--pace-window-ms must be an integer between 0 and 300000') + } return { directorOrigin: DIRECTOR_ORIGIN, cellOrigin: expectedCellOrigin, cellId, - mode: values.mode + mode: values.mode, + paceWindowMs } } @@ -82,24 +96,39 @@ export async function prepareProductionCapacityCell(config, overrides = {}) { const fetchImpl = overrides.fetch ?? fetch const token = overrides.token ?? process.env.ORCA_RELAY_ADMIN_ID_TOKEN if (!token || token.length > 8_192) throw new Error('admin identity token is unavailable') - const postAt = async (origin, path, body) => - await responseJson( - await fetchAdminOnceMore( - fetchImpl, - `${origin}${path}`, - { - method: 'POST', - headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, - body: JSON.stringify(body) - }, - { wait: overrides.wait } - ), - path + const postRaw = async (origin, path, body) => + await fetchAdminOnceMore( + fetchImpl, + `${origin}${path}`, + { + method: 'POST', + headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, + body: JSON.stringify(body) + }, + { wait: overrides.wait } ) + const postAt = async (origin, path, body) => + await responseJson(await postRaw(origin, path, body), path) const post = async (path, body) => await postAt(config.directorOrigin, path, body) if (config.mode === 'drain') { + const paceWindowMs = config.paceWindowMs ?? 0 + if (paceWindowMs > 0) { + const paced = await postRaw(config.cellOrigin, '/v1/admin/drain', { + v: 1, + graceMs: 0, + paceWindowMs + }) + if (paced.ok) { + await paced.json().catch(() => ({})) + return { changed: false, drained: true, paceWindowMs } + } + // A cell still on an image without paced drain rejects the unknown field outright. + // An unpaced drain is the behaviour that cell already has, so fall back to it. + if (paced.status !== 400) throw new Error(`/v1/admin/drain returned ${paced.status}`) + await paced.json().catch(() => ({})) + } await postAt(config.cellOrigin, '/v1/admin/drain', { v: 1, graceMs: 0 }) - return { changed: false, drained: true } + return { changed: false, drained: true, paceWindowMs: 0 } } const before = await inspectAdmissionSelector(post) const state = selectorCellState(before.selector, config.cellId) diff --git a/cloud/dev/scripts/prepare-relay-production-capacity-canary.test.mjs b/cloud/dev/scripts/prepare-relay-production-capacity-canary.test.mjs index 274a60d2198..e721bfb8c75 100644 --- a/cloud/dev/scripts/prepare-relay-production-capacity-canary.test.mjs +++ b/cloud/dev/scripts/prepare-relay-production-capacity-canary.test.mjs @@ -90,7 +90,8 @@ describe('production Relay capacity cell admission', () => { directorOrigin: 'https://relay.onorca.dev', cellOrigin: 'https://c7.relay.onorca.dev', cellId: 'production-gce-c7', - mode: 'isolate' + mode: 'isolate', + paceWindowMs: 0 }) assert.throws(() => parseProductionCapacityCellArguments([ '--director-origin', 'https://relay.onorca.dev', @@ -112,8 +113,12 @@ describe('production Relay capacity cell admission', () => { ]), /not approved/) }) - it('admits the same-cap Asia cells only under the same-cap allowlist', () => { - for (const cellId of ['production-gce-c27', 'production-gce-c28', 'production-gce-c29']) { + it('admits the same-cap Asia and migration-only cells only under the same-cap allowlist', () => { + for (const cellId of [ + 'production-gce-c27', 'production-gce-c28', 'production-gce-c29', + // Migration-only canaries: the US-only capacity rollout never touches them either. + 'production-gce-c17', 'production-gce-c18' + ]) { const hostname = cellId.slice('production-gce-'.length) assert.deepEqual(parseProductionCapacityCellArguments([ '--director-origin', 'https://relay.onorca.dev', @@ -125,10 +130,11 @@ describe('production Relay capacity cell admission', () => { directorOrigin: 'https://relay.onorca.dev', cellOrigin: `https://${hostname}.relay.onorca.dev`, cellId, - mode: 'isolate' + mode: 'isolate', + paceWindowMs: 0 }) } - for (const cellId of ['production-gce-c17', 'production-gce-c18', 'production-gce-c30']) { + for (const cellId of ['production-gce-c12', 'production-gce-c30']) { const hostname = cellId.slice('production-gce-'.length) assert.throws(() => parseProductionCapacityCellArguments([ '--director-origin', 'https://relay.onorca.dev', @@ -168,13 +174,77 @@ describe('production Relay capacity cell admission', () => { { ...config, mode: 'drain' }, { fetch: fake.fetch, token: 'token' } ) - assert.deepEqual(result, { changed: false, drained: true }) + assert.deepEqual(result, { changed: false, drained: true, paceWindowMs: 0 }) assert.deepEqual(fake.calls, [{ path: '/v1/admin/drain', body: { v: 1, graceMs: 0 } }]) }) + it('paces the drain send when the roll asks for a window', async () => { + const fake = canaryFetch() + const result = await prepareProductionCapacityCell( + { ...config, mode: 'drain', paceWindowMs: 120_000 }, + { fetch: fake.fetch, token: 'token' } + ) + assert.deepEqual(result, { changed: false, drained: true, paceWindowMs: 120_000 }) + assert.deepEqual(fake.calls, [{ + path: '/v1/admin/drain', + body: { v: 1, graceMs: 0, paceWindowMs: 120_000 } + }]) + }) + + it('drains unpaced when the cell image rejects the pacing field', async () => { + const bodies = [] + const result = await prepareProductionCapacityCell( + { ...config, mode: 'drain', paceWindowMs: 120_000 }, + { + token: 'token', + wait: async () => {}, + fetch: async (url, init) => { + assert.equal(new URL(url).pathname, '/v1/admin/drain') + const body = JSON.parse(init.body) + bodies.push(body) + if (body.paceWindowMs !== undefined) return response({ error: 'invalid_request' }, 400) + return response({ v: 1, draining: true }) + } + } + ) + assert.deepEqual(result, { changed: false, drained: true, paceWindowMs: 0 }) + assert.deepEqual(bodies, [ + { v: 1, graceMs: 0, paceWindowMs: 120_000 }, + { v: 1, graceMs: 0 } + ]) + }) + + it('fails a paced drain that the cell rejects for any other reason', async () => { + await assert.rejects( + prepareProductionCapacityCell( + { ...config, mode: 'drain', paceWindowMs: 120_000 }, + { + token: 'token', + wait: async () => {}, + fetch: async () => response({ error: 'invalid_token' }, 401) + } + ), + /returned 401/ + ) + }) + + it('refuses a pacing window that is not a bounded integer', () => { + const argv = (value) => [ + '--director-origin', 'https://relay.onorca.dev', + '--cell-origin', 'https://c26.relay.onorca.dev', + '--cell-id', 'production-gce-c26', + '--mode', 'drain', + '--pace-window-ms', value + ] + for (const value of ['-1', '300001', '1.5', 'soon']) { + assert.throws(() => parseProductionCapacityCellArguments(argv(value)), /pace-window-ms/) + } + assert.equal(parseProductionCapacityCellArguments(argv('300000')).paceWindowMs, 300_000) + }) + it('restores only the selected cell to general admission', async () => { const fake = canaryFetch() await prepareProductionCapacityCell( @@ -228,7 +298,7 @@ describe('production Relay capacity cell admission', () => { } ) assert.equal(calls, 2) - assert.deepEqual(result, { changed: false, drained: true }) + assert.deepEqual(result, { changed: false, drained: true, paceWindowMs: 0 }) }) it('fails when both drain attempts return a transient 503', async () => { diff --git a/cloud/dev/scripts/relay-production-same-cap-wave.mjs b/cloud/dev/scripts/relay-production-same-cap-wave.mjs index 4fd1c030d95..654d9fc55bd 100644 --- a/cloud/dev/scripts/relay-production-same-cap-wave.mjs +++ b/cloud/dev/scripts/relay-production-same-cap-wave.mjs @@ -2,14 +2,29 @@ import { readFileSync } from 'node:fs' import { pathToFileURL } from 'node:url' import { requireSameEvidenceCode } from './relay-evidence-code-provenance.mjs' +// Migration-only by policy: zero hosts and no reservation, so a wave rolls one without +// displacing anybody. It enters and must leave migration-only, never general. +export const SAME_CAP_MIGRATION_ONLY_CELLS = ['production-gce-c17', 'production-gce-c18'] + export const SAME_CAP_CELLS = [ 'production-gce-c7', 'production-gce-c8', 'production-gce-c9', 'production-gce-c10', 'production-gce-c13', 'production-gce-c14', 'production-gce-c15', 'production-gce-c16', 'production-gce-c19', 'production-gce-c20', 'production-gce-c21', 'production-gce-c22', 'production-gce-c23', 'production-gce-c24', 'production-gce-c25', 'production-gce-c26', - 'production-gce-c27', 'production-gce-c28', 'production-gce-c29' + 'production-gce-c27', 'production-gce-c28', 'production-gce-c29', + ...SAME_CAP_MIGRATION_ONLY_CELLS ] +// A general cell's wave isolates and restores it, advancing the selector twice; a +// migration-only cell's isolate and restore are both no-ops, so its wave advances nothing. +export function selectorWaveDelta(cellId) { + return SAME_CAP_MIGRATION_ONLY_CELLS.includes(cellId) ? 0 : 2 +} + +export function entryAdmission(cellId) { + return SAME_CAP_MIGRATION_ONLY_CELLS.includes(cellId) ? 'migration-only' : 'general' +} + function digest(value, name) { if (!/^sha256:[a-f0-9]{64}$/.test(value ?? '')) throw new Error(`${name} is invalid`) return value @@ -23,6 +38,11 @@ function cells(value) { new Set(parsed).size !== parsed.length || parsed.some((cell) => !SAME_CAP_CELLS.includes(cell)) ) throw new Error('same-cap wave cells are invalid') + // Every later cell offsets from one per-wave selector delta, and the two classes + // have different ones, so a mixed wave has no single offset any cell could use. + if (new Set(parsed.map(selectorWaveDelta)).size > 1) { + throw new Error('same-cap wave cells must be all general or all migration-only') + } return parsed } @@ -101,7 +121,7 @@ export function canaryAuthority(input) { cellId: wave.cells[0], targetDigest: wave.targetDigest, rollbackDigest: wave.rollbackDigest, - selectorGeneration: selectorGeneration + 2, + selectorGeneration: selectorGeneration + selectorWaveDelta(wave.cells[0]), rehomeGeneration, // Audit trail, not authority: a batch reusing this canary is authorized by // its own confirmation, so verification below neither requires nor forbids it. @@ -114,6 +134,8 @@ export function canaryAuthority(input) { export function verifyCanaryAuthority(authority, expected, repositoryRoot) { const selectorGeneration = Number(expected.selectorGeneration) + // A mixed wave is already rejected, so the batch's first cell names the whole batch's class. + const batchAdmission = entryAdmission(cells(expected.cellIds ?? '')[0]) if ( authority?.v !== 1 || !/^[0-9a-f]{40}$/.test(authority.commitSha ?? '') || @@ -127,6 +149,14 @@ export function verifyCanaryAuthority(authority, expected, repositoryRoot) { authority.rehomeGeneration !== Number(expected.rehomeGeneration) || !SAME_CAP_CELLS.includes(authority.cellId) ) throw new Error('canary authority does not match this batch') + // A migration-only cell carries no hosts and a different cap, so rolling it proves nothing + // about a general batch, and its wave advances a different selector delta. + if (entryAdmission(authority.cellId) !== batchAdmission) { + throw new Error( + `canary authority cell ${authority.cellId} is ${entryAdmission(authority.cellId)}, ` + + `but this batch is ${batchAdmission}` + ) + } // Each cell checks exact live selector state; later batches may reuse this control epoch's canary. requireSameEvidenceCode({ sealedSha: authority.commitSha, @@ -182,10 +212,20 @@ export function main(argv = process.argv.slice(2)) { }))}\n`) return } + if (command === 'cell-class') { + const cellId = input['cell-id'] + if (!SAME_CAP_CELLS.includes(cellId)) throw new Error('same-cap wave cells are invalid') + process.stdout.write(`${JSON.stringify({ + entryAdmission: entryAdmission(cellId), + selectorWaveDelta: selectorWaveDelta(cellId) + })}\n`) + return + } if (command === 'verify-canary') { verifyCanaryAuthority(JSON.parse(readFileSync(input.file, 'utf8')), { commitSha: input['commit-sha'], runId: input['run-id'], + cellIds: input['cell-ids'], targetDigest: input['target-digest'], rollbackDigest: input['rollback-digest'], selectorGeneration: input['selector-generation'], diff --git a/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs b/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs index 8fea3ecc195..0d2ac6f1a96 100644 --- a/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs +++ b/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs @@ -1,14 +1,19 @@ import assert from 'node:assert/strict' -import { execFileSync } from 'node:child_process' +import { execFileSync, spawnSync } from 'node:child_process' import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { test } from 'node:test' import { + SAME_CAP_CELLS, + SAME_CAP_MIGRATION_ONLY_CELLS, canaryAuthority, + entryAdmission, + main, validateSameCapWave, verifyCanaryAuthority } from './relay-production-same-cap-wave.mjs' +import { readRelayWorkflow } from './relay-repository.mjs' const targetDigest = `sha256:${'a'.repeat(64)}` const rollbackDigest = `sha256:${'b'.repeat(64)}` @@ -52,6 +57,82 @@ test('requires one canary or a bounded reviewed batch', () => { }), /cells/) }) +test('rolls the migration-only cells but never mixes the two classes in one wave', () => { + for (const cellId of SAME_CAP_MIGRATION_ONLY_CELLS) { + assert.equal(SAME_CAP_CELLS.includes(cellId), true, cellId) + assert.equal(entryAdmission(cellId), 'migration-only', cellId) + assert.deepEqual(validateSameCapWave({ + mode: 'canary-apply', + cellIds: cellId, + targetDigest, + rollbackDigest, + confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} ${cellId}` + }).cells, [cellId]) + } + const cellIds = 'production-gce-c17,production-gce-c18' + assert.deepEqual(validateSameCapWave({ + mode: 'batch-apply', + cellIds, + targetDigest, + rollbackDigest, + confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} ${cellIds}`, + canaryRunId: '42' + }).cells, ['production-gce-c17', 'production-gce-c18']) + // A mixed wave has no single selector delta for its later cells to offset from. + const mixed = 'production-gce-c7,production-gce-c17' + assert.throws(() => validateSameCapWave({ + mode: 'batch-apply', + cellIds: mixed, + targetDigest, + rollbackDigest, + confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} ${mixed}`, + canaryRunId: '42' + }), /all general or all migration-only/) +}) + +test('seals a migration-only canary at the generation its wave leaves behind', () => { + const seal = (cellId) => canaryAuthority({ + cellIds: cellId, + targetDigest, + rollbackDigest, + confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} ${cellId}`, + commitSha: 'c'.repeat(40), + runId: '42', + selectorGeneration: '11', + rehomeGeneration: '4' + }) + // Isolate and restore are both no-ops on a migration-only cell, so nothing advances. + assert.equal(seal('production-gce-c17').selectorGeneration, 11) + assert.equal(seal('production-gce-c7').selectorGeneration, 13) + // That canary still authorizes a later batch of its own class; it is evidence about the image. + assert.equal(verifyCanaryAuthority(seal('production-gce-c17'), { + commitSha: 'c'.repeat(40), + runId: '42', + cellIds: 'production-gce-c17,production-gce-c18', + targetDigest, + rollbackDigest, + selectorGeneration: '11', + rehomeGeneration: '4' + }).cellId, 'production-gce-c17') +}) + +test('reports each approved cell\'s class and selector delta', () => { + const printed = [] + const write = process.stdout.write.bind(process.stdout) + process.stdout.write = (chunk) => printed.push(String(chunk)) + try { + main(['cell-class', '--cell-id', 'production-gce-c17']) + main(['cell-class', '--cell-id', 'production-gce-c7']) + } finally { + process.stdout.write = write + } + assert.deepEqual(printed.map((line) => JSON.parse(line)), [ + { entryAdmission: 'migration-only', selectorWaveDelta: 0 }, + { entryAdmission: 'general', selectorWaveDelta: 2 } + ]) + assert.throws(() => main(['cell-class', '--cell-id', 'production-gce-c12']), /cells are invalid/) +}) + test('binds rollback confirmation to the exact digest and ordered cells', () => { assert.throws(() => validateSameCapWave({ mode: 'rollback', @@ -94,6 +175,7 @@ test('seals and verifies canary authority for later batches', () => { assert.equal(verifyCanaryAuthority(authority, { commitSha: 'c'.repeat(40), runId: '42', + cellIds: 'production-gce-c8,production-gce-c9', targetDigest, rollbackDigest, selectorGeneration: '13', @@ -102,6 +184,7 @@ test('seals and verifies canary authority for later batches', () => { assert.throws(() => verifyCanaryAuthority(authority, { commitSha: 'd'.repeat(40), runId: '42', + cellIds: 'production-gce-c8,production-gce-c9', targetDigest, rollbackDigest, selectorGeneration: '11', @@ -116,8 +199,8 @@ test('reuses a canary across selector advances only within the same control epoc commitSha: 'c'.repeat(40), runId: '42', selectorGeneration: '11', rehomeGeneration: '4' }) const expected = { - commitSha: 'c'.repeat(40), runId: '42', targetDigest, rollbackDigest, - selectorGeneration: '21', rehomeGeneration: '4' + commitSha: 'c'.repeat(40), runId: '42', cellIds: 'production-gce-c8,production-gce-c9', + targetDigest, rollbackDigest, selectorGeneration: '21', rehomeGeneration: '4' } for (const generation of ['13', '14', '21', '29']) { assert.equal(verifyCanaryAuthority(authority, { @@ -191,6 +274,7 @@ test('a batch trusts a canary sealed by identical code at an ancestor commit', a const verifyAt = (commitSha, repositoryRoot) => verifyCanaryAuthority(authority, { commitSha, runId: '42', + cellIds: 'production-gce-c8,production-gce-c9', targetDigest, rollbackDigest, selectorGeneration: '21', @@ -313,6 +397,7 @@ test('seals the override into the canary authority as audit trail only', () => { const expected = { commitSha: 'f'.repeat(40), runId: '42', + cellIds: 'production-gce-c8,production-gce-c9', targetDigest, rollbackDigest, selectorGeneration: '21', @@ -337,3 +422,124 @@ test('seals the override into the canary authority as audit trail only', () => { 'production-gce-c7' ) }) + +function sealedCanary(cellId) { + return canaryAuthority({ + cellIds: cellId, + targetDigest, + rollbackDigest, + confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} ${cellId}`, + commitSha: 'c'.repeat(40), + runId: '42', + selectorGeneration: '11', + rehomeGeneration: '4' + }) +} + +// Why: a migration-only cell holds zero hosts at a different cap and its wave advances no +// selector, so rolling one is no evidence for a general batch, and the reverse is no evidence +// either. Nothing but the sealed cell id says which class a canary actually proved. +test('refuses a canary sealed on a cell of the other admission class', () => { + const expected = { + commitSha: 'c'.repeat(40), + runId: '42', + targetDigest, + rollbackDigest, + selectorGeneration: '99', + rehomeGeneration: '4' + } + const general = 'production-gce-c8,production-gce-c9' + const migrationOnly = SAME_CAP_MIGRATION_ONLY_CELLS.join(',') + assert.throws( + () => verifyCanaryAuthority(sealedCanary('production-gce-c17'), { + ...expected, cellIds: general + }), + /canary authority cell production-gce-c17 is migration-only, but this batch is general/ + ) + assert.throws( + () => verifyCanaryAuthority(sealedCanary('production-gce-c7'), { + ...expected, cellIds: migrationOnly + }), + /canary authority cell production-gce-c7 is general, but this batch is migration-only/ + ) + assert.equal( + verifyCanaryAuthority(sealedCanary('production-gce-c7'), { + ...expected, cellIds: general + }).cellId, + 'production-gce-c7' + ) + assert.equal( + verifyCanaryAuthority(sealedCanary('production-gce-c17'), { + ...expected, cellIds: migrationOnly + }).cellId, + 'production-gce-c17' + ) + // A caller that names no batch at all gets no verdict, rather than an unchecked class. + assert.throws( + () => verifyCanaryAuthority(sealedCanary('production-gce-c7'), expected), + /same-cap wave cells are invalid/ + ) +}) + +// The dispatch workflow is the only caller, so the class check only binds anything if that +// step actually hands the batch over; run the step's own shell exactly as written. +function verifyCanaryStepScript() { + const dispatch = readRelayWorkflow('deploy-relay-production-same-cap.yml') + const first = ' node dev/scripts/relay-production-same-cap-wave.mjs verify-canary \\\n' + const start = dispatch.indexOf(first) + assert.notEqual(start, -1, 'the dispatch workflow has no verify-canary step') + const last = ' --rehome-generation "${REHOME_GENERATION}"\n' + const end = dispatch.indexOf(last, start) + assert.notEqual(end, -1, 'the verify-canary step does not end at the rehome generation') + return dispatch.slice(start, end + last.length).replace(/^ {10}/gm, '') +} + +async function runVerifyCanaryStep(authority, cellIds) { + const temporary = await mkdtemp(join(tmpdir(), 'relay-same-cap-verify-')) + try { + await mkdir(join(temporary, 'relay-same-cap-canary'), { recursive: true }) + await writeFile( + join(temporary, 'relay-same-cap-canary', 'authority.json'), + JSON.stringify(authority) + ) + return spawnSync('bash', ['-euo', 'pipefail', '-c', verifyCanaryStepScript()], { + cwd: new URL('../..', import.meta.url), + env: { + ...process.env, + RUNNER_TEMP: temporary, + GITHUB_SHA: authority.commitSha, + CANARY_RUN_ID: authority.runId, + CELL_IDS: cellIds, + TARGET_DIGEST: targetDigest, + ROLLBACK_DIGEST: rollbackDigest, + SELECTOR_GENERATION: '99', + REHOME_GENERATION: '4' + }, + encoding: 'utf8' + }) + } finally { + await rm(temporary, { recursive: true, force: true }) + } +} + +test('the batch gate hands its own cells to the canary check', async () => { + const accepted = await runVerifyCanaryStep( + sealedCanary('production-gce-c7'), + 'production-gce-c8,production-gce-c9' + ) + assert.equal(accepted.status, 0, accepted.stderr) + const crossed = await runVerifyCanaryStep( + sealedCanary('production-gce-c17'), + 'production-gce-c8,production-gce-c9' + ) + assert.equal(crossed.status, 1, crossed.stdout) + assert.match( + crossed.stderr, + /canary authority cell production-gce-c17 is migration-only, but this batch is general/ + ) + const migrationOnly = await runVerifyCanaryStep( + sealedCanary('production-gce-c17'), + SAME_CAP_MIGRATION_ONLY_CELLS.join(',') + ) + assert.equal(migrationOnly.status, 0, migrationOnly.stderr) +}) diff --git a/cloud/dev/scripts/relay-regional-rehome-workflow.test.mjs b/cloud/dev/scripts/relay-regional-rehome-workflow.test.mjs index 0168581fd58..e5294124466 100644 --- a/cloud/dev/scripts/relay-regional-rehome-workflow.test.mjs +++ b/cloud/dev/scripts/relay-regional-rehome-workflow.test.mjs @@ -67,11 +67,11 @@ test('same-cap wrapper is reusable, canary-bound, and sequential', () => { ) assert.match( job, - /Require converged Terraform state and a stable MIG on resume[\s\S]{0,200}DIRECTOR_RUNTIME_SERVICE_ACCOUNT: \$\{\{ vars\.PRODUCTION_GCP_RELAY_DIRECTOR_RUNTIME_SERVICE_ACCOUNT \}\}/ + /Require converged Terraform state and a stable MIG on resume[\s\S]{0,300}CAPACITY_SERVICE_ACCOUNT: \$\{\{ vars\.PRODUCTION_GCP_RELAY_CAPACITY_SERVICE_ACCOUNT \}\}\n {10}DIRECTOR_RUNTIME_SERVICE_ACCOUNT: \$\{\{ vars\.PRODUCTION_GCP_RELAY_DIRECTOR_RUNTIME_SERVICE_ACCOUNT \}\}/ ) assert.match( job, - /--rollback-image "\$\{DESIRED_IMAGE\}" \\\n {16}--rehome-director-service-account "\$\{DIRECTOR_RUNTIME_SERVICE_ACCOUNT\}"/ + /--rollback-image "\$\{DESIRED_IMAGE\}" \\\n {16}--capacity-service-account "\$\{CAPACITY_SERVICE_ACCOUNT\}" \\\n {16}--rehome-director-service-account "\$\{DIRECTOR_RUNTIME_SERVICE_ACCOUNT\}"/ ) assert.match( job, @@ -87,7 +87,7 @@ test('same-cap wrapper is reusable, canary-bound, and sequential', () => { assert.match(job, /SELECTOR_GENERATION_AFTER_ISOLATE=\$\{EFFECTIVE_SELECTOR_GENERATION\}/) assert.match(job, /SELECTOR_GENERATION_AFTER_ISOLATE=\$\{ISOLATE_GENERATION\}/) assert.match(job, /--expected-selector-generation "\$\{SELECTOR_GENERATION_AFTER_ISOLATE\}"/) - assert.match(job, /--expected-selector-generation "\$\{SELECTOR_GENERATION_AFTER_ACTIVATE\}"/) + assert.match(job, /--expected-selector-generation "\$\{SELECTOR_GENERATION_AFTER_RESTORE\}"/) assert.match(job, /--expected-migration-only-cells "\$\{RESTORED_MIGRATION_CELLS\}"/) assert.match(job, /--expected-general-cells "\$\{RESTORED_GENERAL_CELLS\}"/) assert.match(job, /FAILSAFE_GENERATION/) @@ -98,14 +98,17 @@ test('same-cap wrapper is reusable, canary-bound, and sequential', () => { // Wave 0 must retry freshness-only failures too: one Cloud Monitoring publish // lag at the sample instant is not health evidence, and single-shot wave 0 // failed a whole batch on a series that was fresh again a minute later. - assert.match(job, /dry-run\.state\.json" \\\n {14}--wave-index "\$\{WAVE_INDEX\}" --retry-freshness/) + assert.match( + job, + /dry-run\.state\.json" \\\n {14}--wave-index "\$\{WAVE_INDEX\}" \\\n {14}--selector-wave-delta "\$\{SELECTOR_WAVE_DELTA\}" --retry-freshness/ + ) assert.doesNotMatch(job, /RETRY_ARGS/) // Break-glass: the override skips the aggregate 15-minute monitor evidence and // nothing else. The live per-wave recheck still runs on the override path, off // the dispatch inputs the rehome inspect below verifies against the director. assert.match( job, - /if test -n "\$\{GATE_OVERRIDE_CONFIRMATION\}"; then[\s\S]{0,700}?--no-monitor-state \\\n {14}--expected-selector-generation "\$\{EXPECTED_SELECTOR_GENERATION\}" \\\n {14}--selector-membership-file[\s\S]{0,120}?--wave-index "\$\{WAVE_INDEX\}" --retry-freshness/ + /if test -n "\$\{GATE_OVERRIDE_CONFIRMATION\}"; then[\s\S]{0,700}?--no-monitor-state \\\n {14}--expected-selector-generation "\$\{EXPECTED_SELECTOR_GENERATION\}" \\\n {14}--selector-membership-file[\s\S]{0,160}?--wave-index "\$\{WAVE_INDEX\}" \\\n {14}--selector-wave-delta "\$\{SELECTOR_WAVE_DELTA\}" --retry-freshness/ ) // The override is re-validated here, not trusted from the caller, and it is // bound to the digest this wave installs. diff --git a/cloud/dev/scripts/relay-same-cap-script-census.test.mjs b/cloud/dev/scripts/relay-same-cap-script-census.test.mjs index 770e20b6353..5324e7489e7 100644 --- a/cloud/dev/scripts/relay-same-cap-script-census.test.mjs +++ b/cloud/dev/scripts/relay-same-cap-script-census.test.mjs @@ -3,7 +3,12 @@ import { spawnSync } from 'node:child_process' import { readFileSync } from 'node:fs' import { describe, it } from 'node:test' import { parseProductionCapacityCellArguments } from './prepare-relay-production-capacity-canary.mjs' -import { SAME_CAP_CELLS } from './relay-production-same-cap-wave.mjs' +import { + SAME_CAP_CELLS, + SAME_CAP_MIGRATION_ONLY_CELLS, + entryAdmission, + selectorWaveDelta +} from './relay-production-same-cap-wave.mjs' import { readRelayWorkflow } from './relay-repository.mjs' import { validateCapacityPlan } from './validate-relay-capacity-plan.mjs' @@ -15,6 +20,7 @@ const production = readFileSync( ) const REHOME_SOURCE_CELLS = rehomeSourceCells() const DIRECTOR_IDENTITY = 'relay-director@onorca-cloud.iam.gserviceaccount.com' +const CAPACITY_IDENTITY = 'orca-cloud-gha-cap@onorca-cloud.iam.gserviceaccount.com' const AUDIENCE = 'https://relay.onorca.dev/v1/admin/host-drain' const ROLLBACK_IMAGE = `us-central1-docker.pkg.dev/p/orca-cloud/relay@sha256:${'d'.repeat(64)}` const TARGET_IMAGE = `us-central1-docker.pkg.dev/p/orca-cloud/relay@sha256:${'e'.repeat(64)}` @@ -33,16 +39,28 @@ function rehomeSourceCells() { // The job cross-checks its pinned pool against the committed map; model the same read. function tfvarsDatabasePoolMax(cellId) { - const start = production.indexOf(`"${cellId}" = {`) - assert.notEqual(start, -1, `${cellId} is missing from production.tfvars`) - const block = production.slice(start, production.indexOf('\n }', start)) - return /database_pool_max\s*=\s*(\d+)/.exec(block)?.[1] ?? '10' + return tfvarsCellBlock(cellId).match(/database_pool_max\s*=\s*(\d+)/)?.[1] ?? '10' } -function startupScript({ cap, image, trusted, pool }) { +function tfvarsHardCap(cellId) { + const cap = /connection_hard_cap\s*=\s*(\d+)/.exec(tfvarsCellBlock(cellId))?.[1] + assert.notEqual(cap, undefined, `${cellId} has no connection_hard_cap`) + return cap +} + +function tfvarsCellBlock(cellId) { + const start = production.indexOf(`"${cellId}" = {`) + assert.notEqual(start, -1, `${cellId} is missing from production.tfvars`) + return production.slice(start, production.indexOf('\n }', start)) +} + +function startupScript({ cap, image, trusted, pool, capacityIdentity = CAPACITY_IDENTITY }) { return [ ` printf 'ORCA_RELAY_CELL_CONNECTION_HARD_CAP=%s\\n' '${cap}'`, ` printf 'ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND=%s\\n' '60'`, + ...(capacityIdentity === null + ? [] + : [` printf 'ORCA_RELAY_CAPACITY_SERVICE_ACCOUNT=%s\\n' '${capacityIdentity}'`]), ...(pool === undefined ? [] : [` printf 'ORCA_RELAY_DATABASE_POOL_MAX=%s\\n' '${pool}'`]), @@ -59,7 +77,11 @@ function startupScript({ cap, image, trusted, pool }) { } // The exact shape the apply step's plan has: template replaced, MIG rebound to it. -function rollPlan({ cellId, cap, protocol, pool }) { +function rollPlan({ + cellId, cap, protocol, pool, + beforeCapacityIdentity = CAPACITY_IDENTITY, + afterCapacityIdentity = CAPACITY_IDENTITY +}) { return { configuration: { root_module: { @@ -90,7 +112,8 @@ function rollPlan({ cellId, cap, protocol, pool }) { image: ROLLBACK_IMAGE, trusted: protocol >= 1, // The live template predates the reviewed pool raise, as every asia cell's does. - pool: pool === undefined ? undefined : '10' + pool: pool === undefined ? undefined : '10', + capacityIdentity: beforeCapacityIdentity }) }, after: { @@ -98,7 +121,8 @@ function rollPlan({ cellId, cap, protocol, pool }) { cap, image: TARGET_IMAGE, trusted: protocol >= 1, - pool + pool, + capacityIdentity: afterCapacityIdentity }), self_link: null }, @@ -145,6 +169,95 @@ function cellShape(cellId) { return { cap: Number(cap), pool: pool.slice('pool='.length) || undefined } } +// The class block runs before checkout-independent work and decides the whole wave shape. +function resolveCellClass(cellId) { + return spawnSync('bash', [ + '-euo', + 'pipefail', + '-c', + `${jobBlock( + ' CELL_CLASS="$(node dev/scripts/relay-production-same-cap-wave.mjs cell-class \\', + ' SELECTOR_WAVE_DELTA="$(jq -er \'.selectorWaveDelta\' <<< "${CELL_CLASS}")"' + )}\necho "\${ENTRY_ADMISSION} \${SELECTOR_WAVE_DELTA}"` + ], { cwd: new URL('../..', import.meta.url), env: { ...process.env, TARGET_CELL_ID: cellId }, encoding: 'utf8' }) +} + +function drainingBlock() { + return `${jobBlock( + ' # Rollback is the documented recovery from a failed canary, which', + ' PREDECESSOR_DRAINING_OK=false\n fi' + )}\necho "\${PRECHECK_ADMISSION} \${PRECHECK_DRAINING} \${PREDECESSOR_DRAINING_OK}"` +} + +// The three fields gcloud would otherwise default, as the MIG resource declares them. +function migUpdatePolicy() { + const terraform = readFileSync( + new URL('../../infra/terraform/relay-gce-cells.tf', import.meta.url), + 'utf8' + ) + const policy = terraform.split(' update_policy {')[1]?.split('\n }')[0] ?? '' + const method = /replacement_method\s+= "([A-Z]+)"/.exec(policy)?.[1] + assert.notEqual(method, undefined, 'the MIG declares no replacement method') + // Both fixed bounds come from the topology locals the MIG resource points at. + const surgeLocal = /max_surge_fixed\s+= local\.relay_gce_topology\.(\w+)/.exec(policy)?.[1] + const unavailableLocal = + /max_unavailable_fixed\s+= local\.relay_gce_topology\.(\w+)/.exec(policy)?.[1] + assert.notEqual(surgeLocal, undefined, 'the MIG pins no surge local') + assert.notEqual(unavailableLocal, undefined, 'the MIG pins no unavailable local') + const topology = terraform.split(' relay_gce_topology = {')[1]?.split('\n }')[0] ?? '' + const local = (name) => { + const value = new RegExp(`${name}\\s+= (\\d+)`).exec(topology)?.[1] + assert.notEqual(value, undefined, `the topology locals pin no ${name}`) + return value + } + return { + replacementMethod: method.toLowerCase(), + maxSurge: local(surgeLocal), + maxUnavailable: local(unavailableLocal) + } +} + +// The stage decides the predecessor, the plan's reviewed rollback image, and whether the +// MIG is rolled explicitly, so run the real block rather than restating its rule. +function stageBlock() { + return `${jobBlock( + ' # Two different failures leave the cell on the rollback image, and the image', + ' PLAN_ROLLBACK_IMAGE="${IMAGE_REPOSITORY}@${CURRENT_IMAGE_DIGEST}"\n fi' + )}\necho "\${ROLLBACK_STAGE} \${ROLLBACK_RESUME} \${PREDECESSOR_IMAGE_DIGEST}` + + ` \${PREDECESSOR_REHOME_PROTOCOL} \${PLAN_ROLLBACK_IMAGE}"` +} + +function generationBlock() { + return `${jobBlock( + ' if test "${DEPLOY_MODE}" = verify; then', + ' fi' + )}\necho "\${EFFECTIVE_SELECTOR_GENERATION}"` +} + +// The job derives both memberships in one block; run that block alone for each class. +function membership(env) { + const script = `${jobBlock( + ' RESTORED_MIGRATION_CELLS="$(jq -rn \\', + ' fi' + )}\njq -cn --arg a "\${ISOLATED_MIGRATION_CELLS}" --arg b "\${ISOLATED_GENERAL_CELLS}" \\ + --arg c "\${RESTORED_MIGRATION_CELLS}" --arg d "\${RESTORED_GENERAL_CELLS}" \\ + '{isolatedMigration:$a,isolatedGeneral:$b,restoredMigration:$c,restoredGeneral:$d}'` + const resolved = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { + env: { ...process.env, ...env }, + encoding: 'utf8' + }) + assert.equal(resolved.status, 0, resolved.stderr) + return JSON.parse(resolved.stdout) +} + +function jobBlock(firstLine, lastLine) { + const start = workflow.indexOf(`${firstLine}\n`) + assert.notEqual(start, -1, `the job has no ${firstLine.trim()}`) + const end = workflow.indexOf(`\n${lastLine}\n`, start) + assert.notEqual(end, -1, `that block has no ${lastLine.trim()}`) + return workflow.slice(start, end + lastLine.length + 1).replace(/^ {10}/gm, '') +} + describe('same-cap roll scripts accept every same-cap cell', () => { it('parses every wave cell through the same-cap canary allowlist', () => { for (const cellId of SAME_CAP_CELLS) { @@ -159,7 +272,8 @@ describe('same-cap roll scripts accept every same-cap cell', () => { directorOrigin: 'https://relay.onorca.dev', cellOrigin: `https://${hostname(cellId)}.relay.onorca.dev`, cellId, - mode + mode, + paceWindowMs: 0 }) } } @@ -171,12 +285,13 @@ describe('same-cap roll scripts accept every same-cap cell', () => { assert.equal(resolved.status, 0, `${cellId}: ${resolved.stderr}`) assert.match( resolved.stdout.trim(), - /^(us-central1 1000 pool=|asia-east2 3000 pool=16)$/, + /^(us-central1 1000 pool=|us-central1 600 pool=|asia-east2 3000 pool=16)$/, cellId ) assert.equal(tfvarsDatabasePoolMax(cellId), cellShape(cellId).pool ?? '10', cellId) + assert.equal(String(cellShape(cellId).cap), tfvarsHardCap(cellId), cellId) } - assert.equal(resolveCellShape('production-gce-c17').status, 1) + assert.equal(resolveCellShape('production-gce-c12').status, 1) assert.equal(resolveCellShape('production-gce-c30').status, 1) }) @@ -188,10 +303,19 @@ describe('same-cap roll scripts accept every same-cap cell', () => { const end = lines.findIndex((line) => !line.endsWith('\\')) const call = lines.slice(0, end + 1).join(' ') assert.match(call, /--approved-cells same-cap/) - assert.match(call, /--mode (isolate|drain|activate)/) + // The restore call picks its mode from the cell's entry admission class. + assert.match(call, /--mode (isolate|drain|activate|"\$\{RESTORE_MODE\}")/) } }) + it('paces the drain it sends to the selected cell', () => { + const drain = workflow.split('--mode drain')[1] ?? '' + assert.match(drain.split('\n').slice(0, 2).join(' '), /--pace-window-ms "\$\{DRAIN_PACE_WINDOW_MS\}"/) + assert.match(workflow, /DRAIN_PACE_WINDOW_MS: '120000'/) + // The transition wait has to outlast the pacing window on top of the leases it waits on. + assert.match(workflow, /--activity restart-safe[\s\S]*?--timeout-ms 1020000/) + }) + it('passes this cell\'s rehome protocol and pool on every plan validation the job runs', () => { const invocations = workflow.split('validate-relay-capacity-plan.mjs').slice(1) assert.equal(invocations.length, 2) @@ -213,9 +337,15 @@ describe('same-cap roll scripts accept every same-cap cell', () => { }) it('validates a correct plan for every wave cell at that cell\'s rehome protocol', () => { - for (const [cellId, protocol] of SAME_CAP_CELLS.flatMap((cell) => [[cell, 1], [cell, 3]])) { + const trusted = SAME_CAP_CELLS.filter((cell) => REHOME_SOURCE_CELLS.has(cell)) + // Only a declared rehome source may roll at a trusted protocol at all; the job refuses + // the rest before it plans, and the next test covers them at protocol 0. + assert.deepEqual( + SAME_CAP_CELLS.filter((cell) => !REHOME_SOURCE_CELLS.has(cell)), + SAME_CAP_MIGRATION_ONLY_CELLS + ) + for (const [cellId, protocol] of trusted.flatMap((cell) => [[cell, 1], [cell, 3]])) { const { cap, pool } = cellShape(cellId) - assert.equal(REHOME_SOURCE_CELLS.has(cellId), true, cellId) const config = { mode: 'same-cap-cell', cellId, @@ -223,6 +353,7 @@ describe('same-cap roll scripts accept every same-cap cell', () => { unobservedBound: 60, image: TARGET_IMAGE, rollbackImage: ROLLBACK_IMAGE, + capacityServiceAccount: CAPACITY_IDENTITY, rehomeDirectorServiceAccount: DIRECTOR_IDENTITY, rehomeAudience: AUDIENCE, regionalRehomeProtocol: String(protocol), @@ -261,15 +392,16 @@ describe('same-cap roll scripts accept every same-cap cell', () => { const config = { mode: 'same-cap-cell', cellId, - hardCap: 1000, + hardCap: 600, unobservedBound: 60, image: TARGET_IMAGE, rollbackImage: ROLLBACK_IMAGE, + capacityServiceAccount: CAPACITY_IDENTITY, rehomeDirectorServiceAccount: DIRECTOR_IDENTITY, rehomeAudience: AUDIENCE, regionalRehomeProtocol: '0' } - const plan = rollPlan({ cellId, cap: 1000, protocol: 0 }) + const plan = rollPlan({ cellId, cap: 600, protocol: 0 }) assert.deepEqual(validateCapacityPlan(plan, config), { mode: 'same-cap-cell', changes: 2 }) // Protocol 1 must reject a plan with no rehome lines, or the absent-line rule decides nothing. assert.throws( @@ -278,6 +410,304 @@ describe('same-cap roll scripts accept every same-cap cell', () => { ) }) + it('resolves the class and selector delta the wave validator declares', () => { + for (const cellId of SAME_CAP_CELLS) { + const resolved = resolveCellClass(cellId) + assert.equal(resolved.status, 0, `${cellId}: ${resolved.stderr}`) + assert.equal( + resolved.stdout.trim(), + `${entryAdmission(cellId)} ${selectorWaveDelta(cellId)}`, + cellId + ) + } + assert.equal(resolveCellClass('production-gce-c12').status, 1) + }) + + it('offsets a later wave by this cell class\'s own selector delta', () => { + for (const [waveIndex, delta] of [['0', 2], ['3', 2], ['0', 0], ['3', 0]]) { + const resolved = spawnSync('bash', ['-euo', 'pipefail', '-c', generationBlock()], { + env: { + ...process.env, + DEPLOY_MODE: 'apply', + EXPECTED_SELECTOR_GENERATION: '40', + WAVE_INDEX: waveIndex, + SELECTOR_WAVE_DELTA: String(delta) + }, + encoding: 'utf8' + }) + assert.equal(resolved.status, 0, resolved.stderr) + assert.equal(resolved.stdout.trim(), String(40 + delta * Number(waveIndex))) + } + }) + + it('hands a migration-only cell back the exact membership it entered with', () => { + const entry = { + EXPECTED_MIGRATION_ONLY_CELLS: 'production-gce-c17,production-gce-c18', + EXPECTED_GENERAL_CELLS: 'production-gce-c7,production-gce-c8' + } + const isolated = membership({ + ...entry, + TARGET_CELL_ID: 'production-gce-c17', + ENTRY_ADMISSION: 'migration-only' + }) + assert.deepEqual(isolated, { + isolatedMigration: 'production-gce-c17,production-gce-c18', + isolatedGeneral: 'production-gce-c7,production-gce-c8', + restoredMigration: 'production-gce-c17,production-gce-c18', + restoredGeneral: 'production-gce-c7,production-gce-c8' + }) + // A general cell still leaves migration-only and returns to general. + assert.deepEqual( + membership({ + ...entry, + TARGET_CELL_ID: 'production-gce-c7', + ENTRY_ADMISSION: 'general' + }), + { + isolatedMigration: 'production-gce-c17,production-gce-c18,production-gce-c7', + isolatedGeneral: 'production-gce-c8', + restoredMigration: 'production-gce-c17,production-gce-c18', + restoredGeneral: 'production-gce-c7,production-gce-c8' + } + ) + }) + + it('never activates a migration-only cell and proves its isolate changed nothing', () => { + const restore = workflow + .split('name: Restore only the verified selected cell to its entry admission')[1] + .split('\n - id:')[0] + assert.match(restore, /if test "\$\{ENTRY_ADMISSION\}" = migration-only; then\n\s+RESTORE_MODE=isolate/) + assert.match(restore, /--admission "\$\{ENTRY_ADMISSION\}"/) + // The pre-mutation check must demand the class the cell is declared to serve in. + assert.match(workflow, /PRECHECK_ADMISSION="\$\{ENTRY_ADMISSION\}"/) + const isolate = workflow + .split('name: Reversibly isolate and drain only the selected cell')[1] + .split('\n - id:')[0] + assert.match(isolate, /migration-only; then\n\s+jq -e '\.changed == false'/) + }) + + it('requires rehome source membership exactly when a roll carries trust lines', () => { + const step = workflow + .split('name: Resolve immutable same-cap cell configuration')[1] + .split('\n - name:')[0] + const guard = step.indexOf('jq -e --arg cell "${TARGET_CELL_ID}" \'index($cell) != null\'') + assert.notEqual(guard, -1) + // The guard reads both protocols, so it has to sit after they are resolved. + assert.ok(step.indexOf('DESIRED_REHOME_PROTOCOL="${TARGET_REHOME_PROTOCOL}"') < guard) + assert.match( + step.slice(0, guard), + /test "\$\{DESIRED_REHOME_PROTOCOL\}" != 0 \|\| test "\$\{CURRENT_REHOME_PROTOCOL\}" != 0\n\s+\}; then\s+$/ + ) + }) + + it('rolls a template stale enough to predate the pinned capacity identity', () => { + // Exactly c17's shape on 2026-09-18: its live template is from 2026-08-07 and has no + // capacity identity line, so the roll adds one. Run 35290908836 failed closed here. + const cellId = 'production-gce-c17' + const config = { + mode: 'same-cap-cell', + cellId, + hardCap: 600, + unobservedBound: 60, + image: TARGET_IMAGE, + rollbackImage: ROLLBACK_IMAGE, + capacityServiceAccount: CAPACITY_IDENTITY, + rehomeDirectorServiceAccount: DIRECTOR_IDENTITY, + rehomeAudience: AUDIENCE, + regionalRehomeProtocol: '0' + } + const stale = rollPlan({ cellId, cap: 600, protocol: 0, beforeCapacityIdentity: null }) + assert.deepEqual(validateCapacityPlan(stale, config), { mode: 'same-cap-cell', changes: 2 }) + // The line may only be gained. A roll may not rewrite it, + assert.throws( + () => validateCapacityPlan(stale, { + ...config, + capacityServiceAccount: 'orca-cloud-gha-other@onorca-cloud.iam.gserviceaccount.com' + }), + /reviewed image and capacity/ + ) + // nor drop it from a template that already carries one. + assert.throws( + () => validateCapacityPlan( + rollPlan({ cellId, cap: 600, protocol: 0, afterCapacityIdentity: null }), + config + ), + /reviewed image and capacity/ + ) + // A same-cap roll cannot run without the identity pinned at all. + assert.throws( + () => validateCapacityPlan(stale, { ...config, capacityServiceAccount: undefined }), + /invalid service account/ + ) + }) + + it('pins the capacity identity on every plan validation the job runs', () => { + const invocations = workflow.split('validate-relay-capacity-plan.mjs').slice(1) + assert.equal(invocations.length, 2) + for (const invocation of invocations) { + const lines = invocation.split('\n') + const end = lines.findIndex((line) => !line.trimEnd().endsWith('\\')) + assert.match( + lines.slice(0, end + 1).join(' '), + /--capacity-service-account "\$\{CAPACITY_SERVICE_ACCOUNT\}"/ + ) + } + // Both steps must read it from the same repository variable the job already requires. + assert.equal( + workflow.split( + 'CAPACITY_SERVICE_ACCOUNT: ${{ vars.PRODUCTION_GCP_RELAY_CAPACITY_SERVICE_ACCOUNT }}' + ).length, + 4 + ) + }) + + it('decides the predecessor draining rule from the real block, for both classes', () => { + // A zero-host cell sheds nothing, and a failed canary's own drain leaves the flag set + // with no restart behind it; run 35292335415 stopped on exactly that residue. + const cases = [ + // mode, entry class, resume, expected [precheck admission, precheck draining, jq ok] + ['apply', 'migration-only', 'false', ['migration-only', 'either', 'true']], + ['apply', 'general', 'false', ['general', 'forbidden', 'false']], + ['verify', 'migration-only', 'false', ['migration-only', 'either', 'true']], + ['verify', 'general', 'false', ['general', 'forbidden', 'false']], + // Every rollback path keeps exactly the behaviour it had. + ['rollback', 'general', 'false', ['general-or-migration-only', 'either', 'true']], + ['rollback', 'general', 'true', ['general-or-migration-only', 'either', 'false']], + ['rollback', 'migration-only', 'false', ['general-or-migration-only', 'either', 'true']], + ['rollback', 'migration-only', 'true', ['general-or-migration-only', 'either', 'false']] + ] + for (const [mode, entry, resume, expected] of cases) { + const resolved = spawnSync('bash', ['-euo', 'pipefail', '-c', drainingBlock()], { + env: { + ...process.env, + DEPLOY_MODE: mode, + ENTRY_ADMISSION: entry, + ROLLBACK_RESUME: resume + }, + encoding: 'utf8' + }) + assert.equal(resolved.status, 0, `${mode}/${entry}/${resume}: ${resolved.stderr}`) + assert.deepEqual( + resolved.stdout.trim().split(' '), + expected, + `${mode}/${entry}/${resume}` + ) + } + }) + + it('reads one draining decision in both predecessor checks', () => { + const step = workflow + .split('name: Verify exact current generation, digest, cap, and rollback point')[1] + .split('\n - name:')[0] + // The jq assertion and its diagnostic must not be able to disagree. + assert.equal(step.split('--argjson drainingOk "${PREDECESSOR_DRAINING_OK}"').length, 3) + assert.doesNotMatch(step, /drainingOk "\$\(test/) + // The fresh VM is still required not to be draining, on every path. + const after = workflow + .split('name: Verify new incarnation, exact image, protocol, and durable safety')[1] + .split('\n - name:')[0] + assert.match(after, /--admission migration-only --draining forbidden/) + const restore = workflow + .split('name: Restore only the verified selected cell to its entry admission')[1] + .split('\n - id:')[0] + assert.match(restore, /--draining forbidden --activity allowed/) + }) + + it('classifies every rollback stage from the real block', () => { + const repository = 'us-central1-docker.pkg.dev/onorca-cloud/orca-cloud/relay' + const target = `sha256:${'7'.repeat(64)}` + const rollback = `sha256:${'0'.repeat(64)}` + const stage = (mode, live, draining) => { + // Exactly how the job assigns them: rollback swaps desired and current. + const desired = mode === 'rollback' ? rollback : target + const current = mode === 'rollback' ? target : rollback + const resolved = spawnSync('bash', ['-euo', 'pipefail', '-c', stageBlock()], { + env: { + ...process.env, + DEPLOY_MODE: mode, + CURRENT_RUNTIME: JSON.stringify({ imageDigest: live, draining }), + DESIRED_IMAGE_DIGEST: desired, + CURRENT_IMAGE_DIGEST: current, + DESIRED_IMAGE: `${repository}@${desired}`, + IMAGE_REPOSITORY: repository, + DESIRED_REHOME_PROTOCOL: '1', + CURRENT_REHOME_PROTOCOL: '0' + }, + encoding: 'utf8' + }) + assert.equal(resolved.status, 0, `${mode}/${live}/${draining}: ${resolved.stderr}`) + return resolved.stdout.trim().split(' ') + } + const roll = (current, protocol) => + ['roll', 'false', current, protocol, `${repository}@${current}`] + // Only the last row differs from main: it used to read `resume` and wedge, because the + // resume path refuses a draining cell and never restarts one. + assert.deepEqual(stage('apply', rollback, false), roll(rollback, '0')) + assert.deepEqual(stage('apply', rollback, true), roll(rollback, '0')) + assert.deepEqual(stage('apply', target, false), roll(rollback, '0')) + assert.deepEqual(stage('verify', rollback, false), roll(rollback, '0')) + assert.deepEqual(stage('rollback', target, false), roll(target, '0')) + assert.deepEqual(stage('rollback', target, true), roll(target, '0')) + assert.deepEqual( + stage('rollback', rollback, false), + ['resume', 'true', rollback, '1', `${repository}@${target}`] + ) + assert.deepEqual( + stage('rollback', rollback, true), + ['stranded', 'false', rollback, '1', `${repository}@${rollback}`] + ) + // A runtime that reports no drain flag at all must never read as stranded. + const [missing] = stage('rollback', rollback, null) + assert.equal(missing, 'resume') + }) + + it('rolls the MIG itself when a stranded plan changes nothing', () => { + const apply = workflow + .split('name: Apply only the selected same-cap template and MIG')[1] + .split('\n - id:')[0] + // The plan is reviewed against the image the cell serves, not an assumed predecessor. + assert.match(apply, /--rollback-image "\$\{PLAN_ROLLBACK_IMAGE\}"/) + assert.doesNotMatch(apply, /--rollback-image "\$\{IMAGE_REPOSITORY\}/) + assert.match( + apply, + /test "\$\{ROLLBACK_STAGE\}" = stranded \\\n\s+&& test "\$\(jq -er '\.changes' <<< "\$\{PLAN_REVIEW\}"\)" = 0/ + ) + // gcloud persists all three fields into the MIG's update policy and defaults the + // method to substitute here, so every one has to match what Terraform declares or the + // recovery drifts the policy and the next targeted plan is refused as an unreviewed + // MIG change. Read the declared values rather than restating them. + assert.match(apply, /rolling-action replace "\$\{MIG_NAME\}"/) + const declared = migUpdatePolicy() + assert.deepEqual(declared, { + replacementMethod: 'recreate', + maxSurge: '0', + maxUnavailable: '1' + }) + assert.match( + apply, + new RegExp( + `--replacement-method ${declared.replacementMethod}` + + ` --max-surge ${declared.maxSurge} --max-unavailable ${declared.maxUnavailable}` + ) + ) + // Nothing else may reach the group, and the roll has to be waited on. + assert.equal(apply.split('rolling-action').length, 2) + assert.equal(apply.split('wait-until "${MIG_NAME}" --stable').length, 3) + }) + + it('waits on the image a stranded cell actually serves', () => { + const isolate = workflow + .split('name: Reversibly isolate and drain only the selected cell')[1] + .split('\n - id:')[0] + assert.match(isolate, /--expected-image-digests "\$\{PREDECESSOR_IMAGE_DIGEST\}"/) + // A stranded cell has to come back on a new process, which is what clears the drain. + const after = workflow + .split('name: Verify new incarnation, exact image, protocol, and durable safety')[1] + .split('\n - name:')[0] + assert.match(after, /test "\$\{TARGET_INCARNATION\}" != "\$\{SOURCE_INCARNATION\}"/) + assert.match(after, /if test "\$\{ROLLBACK_RESUME\}" = true; then/) + }) + it('leaves the US-only capacity job on the default allowlist', () => { assert.doesNotMatch(capacityWorkflow, /--approved-cells/) }) diff --git a/cloud/dev/scripts/validate-relay-capacity-plan.mjs b/cloud/dev/scripts/validate-relay-capacity-plan.mjs index 3378d78704d..c81cfc8ca55 100644 --- a/cloud/dev/scripts/validate-relay-capacity-plan.mjs +++ b/cloud/dev/scripts/validate-relay-capacity-plan.mjs @@ -47,7 +47,10 @@ export function parseCapacityPlanArguments(argv) { return value } if (!values.image) throw new Error('missing --image') - if (values.mode === 'bootstrap-cell' && !values['capacity-service-account']) { + if ( + ['bootstrap-cell', 'same-cap-cell'].includes(values.mode) && + !values['capacity-service-account'] + ) { throw new Error('missing --capacity-service-account') } if ( @@ -239,7 +242,10 @@ function requireDesiredStartupScript(script, config) { ` printf 'ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND=%s\\n' '${config.unobservedBound}'` ] ] - if (config.mode === 'bootstrap-cell') { + // A same-cap cell whose template predates this line gains it on its next roll, so the + // before/after comparison ignores it; pinning the exact identity here is what reviews it, + // and what stops a roll dropping or rewriting the line it lets through. + if (['bootstrap-cell', 'same-cap-cell'].includes(config.mode)) { expected.push([ /^ printf 'ORCA_RELAY_CAPACITY_SERVICE_ACCOUNT=%s\\n' '[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z0-9-]+\.iam\.gserviceaccount\.com'$/, ` printf 'ORCA_RELAY_CAPACITY_SERVICE_ACCOUNT=%s\\n' '${config.capacityServiceAccount}'` @@ -454,18 +460,22 @@ function cellPlan(plan, changes, config) { const sameCap = ['same-cap-cell', 'same-cap-image'].includes(config.mode) // Only a pinned pool may move here; requireDesiredStartupScript holds the after value exactly. const stripPool = config.mode === 'same-cap-cell' && config.databasePoolMax !== undefined + // Only a template stale enough to predate the line may move it, and only by gaining it; + // requireDesiredStartupScript holds the after value to the exact reviewed identity. + const stripCapacityIdentity = + ['bootstrap-cell', 'same-cap-cell'].includes(config.mode) if ( typeof beforeScript !== 'string' || (sameCap && relayImage(beforeScript) !== config.rollbackImage) || normalizedStartupScript( beforeScript, - config.mode === 'bootstrap-cell', + stripCapacityIdentity, config.mode === 'same-cap-cell', sameCap, stripPool ) !== normalizedStartupScript( script, - config.mode === 'bootstrap-cell', + stripCapacityIdentity, config.mode === 'same-cap-cell', sameCap, stripPool @@ -501,7 +511,7 @@ export function validateCapacityPlan(plan, config) { throw new Error('capacity Terraform plans may change only a cell') } if ( - config.mode === 'bootstrap-cell' && + ['bootstrap-cell', 'same-cap-cell'].includes(config.mode) && !SERVICE_ACCOUNT_EMAIL.test(config.capacityServiceAccount ?? '') ) { throw new Error('capacity Terraform plan has an invalid service account') diff --git a/cloud/dev/scripts/validate-relay-capacity-plan.test.mjs b/cloud/dev/scripts/validate-relay-capacity-plan.test.mjs index 2953e0c2f92..9094e9e0e7a 100644 --- a/cloud/dev/scripts/validate-relay-capacity-plan.test.mjs +++ b/cloud/dev/scripts/validate-relay-capacity-plan.test.mjs @@ -428,10 +428,14 @@ test('same-cap mode preserves 1000/60 while adding only the reviewed trust confi const rollbackImage = `us-docker.pkg.dev/project/relay/image@sha256:${'d'.repeat(64)}` const image = `us-docker.pkg.dev/project/relay/image@sha256:${'e'.repeat(64)}` const directorIdentity = 'relay-director@project.iam.gserviceaccount.com' + const capacityIdentity = 'orca-cloud-gha-cap@project.iam.gserviceaccount.com' const audience = 'https://relay.example.com/v1/admin/host-drain' - const startup = ({ selectedImage, cap = 1_000, trust = false }) => [ + const startup = ({ selectedImage, cap = 1_000, trust = false, capacity = capacityIdentity }) => [ ` printf 'ORCA_RELAY_CELL_CONNECTION_HARD_CAP=%s\\n' '${cap}'`, ` printf 'ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND=%s\\n' '60'`, + ...(capacity === null + ? [] + : [` printf 'ORCA_RELAY_CAPACITY_SERVICE_ACCOUNT=%s\\n' '${capacity}'`]), ...(trust ? [ ` printf 'ORCA_RELAY_REHOME_DIRECTOR_SERVICE_ACCOUNT=%s\\n' '${directorIdentity}'`, ` printf 'ORCA_RELAY_REHOME_AUDIENCE=%s\\n' '${audience}'` @@ -468,6 +472,7 @@ test('same-cap mode preserves 1000/60 while adding only the reviewed trust confi mode: 'same-cap-cell', image, rollbackImage, + capacityServiceAccount: capacityIdentity, rehomeDirectorServiceAccount: directorIdentity, rehomeAudience: audience, regionalRehomeProtocol: '1' @@ -653,10 +658,12 @@ test('protocol-0 same-cap cells roll without rehome trust lines', () => { const rollbackImage = `us-docker.pkg.dev/project/relay/image@sha256:${'d'.repeat(64)}` const image = `us-docker.pkg.dev/project/relay/image@sha256:${'e'.repeat(64)}` const directorIdentity = 'relay-director@project.iam.gserviceaccount.com' + const capacityIdentity = 'orca-cloud-gha-cap@project.iam.gserviceaccount.com' const audience = 'https://relay.example.com/v1/admin/host-drain' const startup = ({ selectedImage, trust = false }) => [ ` printf 'ORCA_RELAY_CELL_CONNECTION_HARD_CAP=%s\\n' '3000'`, ` printf 'ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND=%s\\n' '60'`, + ` printf 'ORCA_RELAY_CAPACITY_SERVICE_ACCOUNT=%s\\n' '${capacityIdentity}'`, ` printf 'ORCA_RELAY_CELL_REGION=%s\\n' 'asia-east2'`, ...(trust ? [ ` printf 'ORCA_RELAY_REHOME_DIRECTOR_SERVICE_ACCOUNT=%s\\n' '${directorIdentity}'`, @@ -693,6 +700,7 @@ test('protocol-0 same-cap cells roll without rehome trust lines', () => { mode: 'same-cap-cell', image, rollbackImage, + capacityServiceAccount: capacityIdentity, rehomeDirectorServiceAccount: directorIdentity, rehomeAudience: audience, regionalRehomeProtocol: '0' @@ -737,6 +745,133 @@ test('protocol-0 same-cap cells roll without rehome trust lines', () => { } }) +test('a same-cap roll may gain the pinned capacity identity but never move it', () => { + const rollbackImage = `us-docker.pkg.dev/project/relay/image@sha256:${'d'.repeat(64)}` + const image = `us-docker.pkg.dev/project/relay/image@sha256:${'e'.repeat(64)}` + const directorIdentity = 'relay-director@project.iam.gserviceaccount.com' + const capacityIdentity = 'orca-cloud-gha-cap@project.iam.gserviceaccount.com' + const audience = 'https://relay.example.com/v1/admin/host-drain' + const startup = ({ selectedImage, capacity }) => [ + ` printf 'ORCA_RELAY_CELL_CONNECTION_HARD_CAP=%s\\n' '600'`, + ` printf 'ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND=%s\\n' '60'`, + ...(capacity === null + ? [] + : [` printf 'ORCA_RELAY_CAPACITY_SERVICE_ACCOUNT=%s\\n' '${capacity}'`]), + `printf 'ORCA_RELAY_IMAGE_DIGEST=%s\\n' '${selectedImage.split('@')[1]}'`, + `docker pull '${selectedImage}'`, + 'docker run --detach \\', + ' --name orca-relay \\', + ` '${selectedImage}'` + ].join('\n') + const plan = (beforeCapacity, afterCapacity) => ({ + resource_changes: [ + { + address: 'google_compute_instance_template.relay_gce_cell["production-gce-c17"]', + change: { + actions: ['create', 'delete'], + before: { + metadata_startup_script: startup({ + selectedImage: rollbackImage, + capacity: beforeCapacity + }) + }, + after: { + metadata_startup_script: startup({ selectedImage: image, capacity: afterCapacity }), + self_link: null + }, + after_unknown: { self_link: true } + } + }, + { + address: 'google_compute_instance_group_manager.relay_gce_cell["production-gce-c17"]', + change: { + actions: ['update'], + before: { target_size: 1, version: [{ instance_template: 'old' }] }, + after: { target_size: 1, version: [{ instance_template: null }] }, + after_unknown: { version: [{ instance_template: true }] } + } + } + ] + }) + const config = { + cellId: 'production-gce-c17', + hardCap: 600, + unobservedBound: 60, + mode: 'same-cap-cell', + image, + rollbackImage, + capacityServiceAccount: capacityIdentity, + rehomeDirectorServiceAccount: directorIdentity, + rehomeAudience: audience, + regionalRehomeProtocol: '0' + } + // A template old enough to predate the line gains it, which is the only move allowed. + assert.deepEqual( + validateCapacityPlan(plan(null, capacityIdentity), config), + { mode: 'same-cap-cell', changes: 2 } + ) + assert.deepEqual( + validateCapacityPlan(plan(capacityIdentity, capacityIdentity), config), + { mode: 'same-cap-cell', changes: 2 } + ) + for (const [before, after] of [ + [capacityIdentity, null], + [null, null], + [capacityIdentity, 'orca-cloud-gha-other@project.iam.gserviceaccount.com'], + [null, 'orca-cloud-gha-other@project.iam.gserviceaccount.com'] + ]) { + assert.throws( + () => validateCapacityPlan(plan(before, after), config), + /reviewed image and capacity/, + `${before} -> ${after}` + ) + } + // Without the pin there is nothing reviewing the line the comparison now ignores. + assert.throws( + () => validateCapacityPlan(plan(null, capacityIdentity), { + ...config, + capacityServiceAccount: undefined + }), + /invalid service account/ + ) + assert.throws( + () => validateCapacityPlan(plan(null, capacityIdentity), { + ...config, + capacityServiceAccount: 'not-an-email' + }), + /invalid service account/ + ) +}) + +test('the capacity identity argument is required by same-cap-cell mode', () => { + const image = `us-docker.pkg.dev/project/relay/image@sha256:${'e'.repeat(64)}` + const rollbackImage = `us-docker.pkg.dev/project/relay/image@sha256:${'d'.repeat(64)}` + const base = [ + '--mode', 'same-cap-cell', + '--cell-id', 'production-gce-c17', + '--hard-cap', '600', + '--unobserved-bound', '60', + '--image', image, + '--rollback-image', rollbackImage, + '--rehome-director-service-account', 'relay-director@project.iam.gserviceaccount.com', + '--rehome-audience', 'https://relay.onorca.dev/v1/admin/host-drain', + '--regional-rehome-protocol', '0' + ] + assert.throws(() => parseCapacityPlanArguments(base), /missing --capacity-service-account/) + assert.throws( + () => parseCapacityPlanArguments([...base, '--capacity-service-account', 'nope']), + /--capacity-service-account is invalid/ + ) + assert.equal( + parseCapacityPlanArguments([ + ...base, + '--capacity-service-account', + 'orca-cloud-gha-cap@project.iam.gserviceaccount.com' + ]).capacityServiceAccount, + 'orca-cloud-gha-cap@project.iam.gserviceaccount.com' + ) +}) + test('the rehome protocol argument is required by same-cap-cell mode alone', () => { const image = `us-docker.pkg.dev/project/relay/image@sha256:${'e'.repeat(64)}` const rollbackImage = `us-docker.pkg.dev/project/relay/image@sha256:${'d'.repeat(64)}` @@ -747,6 +882,7 @@ test('the rehome protocol argument is required by same-cap-cell mode alone', () '--unobserved-bound', '60', '--image', image, '--rollback-image', rollbackImage, + '--capacity-service-account', 'orca-cloud-gha-cap@project.iam.gserviceaccount.com', '--rehome-director-service-account', 'relay-director@project.iam.gserviceaccount.com', '--rehome-audience', 'https://relay.onorca.dev/v1/admin/host-drain', ...extra @@ -788,10 +924,12 @@ test('the reviewed database pool is pinned for the cells that emit one', () => { const rollbackImage = `us-docker.pkg.dev/project/relay/image@sha256:${'d'.repeat(64)}` const image = `us-docker.pkg.dev/project/relay/image@sha256:${'e'.repeat(64)}` const directorIdentity = 'relay-director@project.iam.gserviceaccount.com' + const capacityIdentity = 'orca-cloud-gha-cap@project.iam.gserviceaccount.com' const audience = 'https://relay.example.com/v1/admin/host-drain' const startup = ({ selectedImage, pool }) => [ ` printf 'ORCA_RELAY_CELL_CONNECTION_HARD_CAP=%s\\n' '3000'`, ` printf 'ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND=%s\\n' '60'`, + ` printf 'ORCA_RELAY_CAPACITY_SERVICE_ACCOUNT=%s\\n' '${capacityIdentity}'`, ...(pool === undefined ? [] : [` printf 'ORCA_RELAY_DATABASE_POOL_MAX=%s\\n' '${pool}'`]), @@ -837,6 +975,7 @@ test('the reviewed database pool is pinned for the cells that emit one', () => { mode: 'same-cap-cell', image, rollbackImage, + capacityServiceAccount: capacityIdentity, rehomeDirectorServiceAccount: directorIdentity, rehomeAudience: audience, regionalRehomeProtocol: '1' @@ -891,6 +1030,7 @@ test('the database pool argument is accepted by same-cap-cell mode alone', () => '--unobserved-bound', '60', '--image', image, '--rollback-image', rollbackImage, + '--capacity-service-account', 'orca-cloud-gha-cap@project.iam.gserviceaccount.com', '--rehome-director-service-account', 'relay-director@project.iam.gserviceaccount.com', '--rehome-audience', 'https://relay.onorca.dev/v1/admin/host-drain', '--regional-rehome-protocol', '1', diff --git a/cloud/docs/relay-workflows.md b/cloud/docs/relay-workflows.md index 0915d703c8b..d81620b837d 100644 --- a/cloud/docs/relay-workflows.md +++ b/cloud/docs/relay-workflows.md @@ -374,7 +374,8 @@ remain general at 1,000/60. The workflow lock, single-use evidence marker, exact targeted Terraform plan, and per-cell heartbeat/admission oracle are unchanged. `Deploy Relay Production Same-Cap` rolls only the reviewed US 1,000/60 and Asia 3,000/60 serving -sets without changing a cell's connection shape. Use `canary-apply` for exactly one cell. A successful canary +sets and the two migration-only US 600/60 cells, C17 and C18, without changing a cell's connection +shape. Use `canary-apply` for exactly one cell. A successful canary seals its commit, target and rollback digests, selector generation, and durable rehome generation; `batch-apply` accepts only that same authority and rolls two to four cells sequentially. Each cell is isolated, drained to two restart-safe samples, replaced from a targeted saved plan, and restored only @@ -384,6 +385,65 @@ director; the workflow never receives or mints a director or stamped-cell runtim keeps only the selected cell migration-only, while the exact rollback digest remains dispatchable via the same workflow's `rollback` mode. +A roll holds the whole startup script identical before and after except the image, so a +template stale enough to predate a pinned line fails closed rather than absorbing the drift. +The one exception is the capacity identity: a cell that predates it gains it on its next roll, +and the plan validator pins the exact reviewed identity instead of comparing that line, so a +roll can never drop or rewrite it. Any other stale line still fails closed and needs a +convergence apply first. + +A drained cell is refused before a roll, because draining means something is already +shedding its connections. A migration-only cell has none to shed, so the flag decides nothing +there and is accepted on entry; the replacement VM is still required not to be draining, and +the incarnation check still proves it was replaced. That also unwedges the state a failed +canary leaves behind, where the wave's own drain set the flag and no restart followed. + +C17 and C18 hold no hosts and are not general, so rolling one displaces nobody: they are the +zero-displacement canary for a new image. Their wave enters and leaves migration-only, so its +isolate and its restore are both no-ops and the selector generation does not move; a general +cell's wave still advances it by two. One wave may not mix the two classes, because every cell +after the first offsets from a single per-wave delta. Neither cell is a declared regional-rehome +source, so its template carries no rehome trust lines and it may roll only at rehome protocol `0`; +the job refuses a trusted protocol for it before it plans anything. + +### Recovering a wave that died after its drain + +A cell's drain flag is a one-way latch on the running process. Only a restart clears it, and +the failsafe that isolates a failed cell does not restart anything. So a wave that stopped +any time after its drain step leaves the cell migration-only and draining, and it stays that +way until the cell is rolled. + +Read the failed run before dispatching anything. If its log has a +`"event":"relay_production_capacity_canary","mode":"drain"` line for the cell, the cell is +drained. Then read the cell's live runtime image from +`POST https://.relay.onorca.dev/v1/admin/runtime-status`. + +1. **Do not re-dispatch `apply`.** It requires the cell general and not draining, and a + drained cell is neither. It will fail closed at the predecessor check. +2. **Dispatch `rollback`,** with the same `target-image-digest` and `rollback-image-digest` + the failed wave used, the live selector generation, and the live tri-state membership + with the failed cell listed under migration-only. The confirmation is + `ROLL_BACK_RELAY_SAME_CAP `. +3. The job classifies the cell itself and needs no extra input: + - serving the **rollback** image and draining, it is `stranded`. The wave stopped before + or during its template apply. The job re-isolates, re-drains, applies the reviewed + template, and rolls the MIG explicitly if that template was already in place. The cell + comes back on a new instance, so the drain clears, and it is restored to its entry class. + - serving the **target** image, it is `roll`, the ordinary rollback. The template applied + and the instance was replaced. + - serving the **rollback** image and not draining, it is `resume`: a rollback that failed + after its own template apply. Nothing is applied and nothing restarts. +4. Rollback takes exactly one cell per dispatch. Recover the cells one at a time. +5. If the run died inside `wait-until stable`, the MIG is still rolling on its own. Wait for + it to settle and re-read the runtime before dispatching, or the stage will be read off a + state that is about to change. +6. A `stranded` dispatch that fails at plan review means the template already carries the + target image while the old instance is still up. Wait for the MIG to finish replacing it, + then dispatch again; it will classify as `roll`. + +A mutating dispatch still needs a fresh aggregate monitor dry-run unless the break-glass +override below is used. + ### Gate override (break-glass) Every mutating same-cap wave normally consumes a fresh 15-minute aggregate monitor dry-run. diff --git a/cloud/infra/terraform/relay-observability.tf b/cloud/infra/terraform/relay-observability.tf index 6b62f3a17a5..e1951eede53 100644 --- a/cloud/infra/terraform/relay-observability.tf +++ b/cloud/infra/terraform/relay-observability.tf @@ -60,6 +60,10 @@ locals { control_renewal_latency_ms_p50 = { field = "controlRenewalLatencyMsP50", description = "Control renewal latency p50 in the interval." } control_renewal_latency_ms_p95 = { field = "controlRenewalLatencyMsP95", description = "Control renewal latency p95 in the interval." } control_renewal_latency_ms_max = { field = "controlRenewalLatencyMsMax", description = "Maximum control renewal latency in the interval." } + control_renewal_flushes = { field = "controlRenewalFlushesDelta", description = "Batched control-renewal statements issued in the interval, one per cell per flush window." } + control_renewal_flush_rows_max = { field = "controlRenewalFlushRowsMax", description = "Largest number of hosts renewed by a single statement in the interval; the row ceiling is what bounds how long one flush holds its row locks." } + control_renewal_flush_ms_p95 = { field = "controlRenewalFlushLatencyMsP95", description = "Batched control-renewal statement duration p95 in the interval. Row locks live until the statement commits, so this is the lock hold." } + control_renewal_flush_ms_max = { field = "controlRenewalFlushLatencyMsMax", description = "Maximum batched control-renewal statement duration in the interval." } control_renewals = { field = "controlRenewalsDelta", description = "Control renewal attempts in the interval." } control_renewal_successes = { field = "controlRenewalSuccessesDelta", description = "Successful control renewals in the interval." } control_renewal_lease_misses = { field = "controlRenewalLeaseMissesDelta", description = "Control renewals that found their activity lease missing." } diff --git a/cloud/packages/postgres-schema/src/apply-postgres-schema.test.ts b/cloud/packages/postgres-schema/src/apply-postgres-schema.test.ts index b576e3a1129..85aaf0f5b76 100644 --- a/cloud/packages/postgres-schema/src/apply-postgres-schema.test.ts +++ b/cloud/packages/postgres-schema/src/apply-postgres-schema.test.ts @@ -84,7 +84,7 @@ describe('applyPostgresSchema classification', () => { wait: async () => undefined }) expect(query).toHaveBeenCalledTimes(3) - expect(summary).toEqual({ ran: 1, skipped: 0 }) + expect(summary).toEqual({ ran: 1, skipped: 0, deferred: 0 }) }) it('treats an already-applied constraint as skipped rather than an error', async () => { @@ -94,7 +94,110 @@ describe('applyPostgresSchema classification', () => { throw postgresError('42710') }) const summary = await applyPostgresSchema(['ALTER TABLE t ADD CONSTRAINT c CHECK (x > 0)'], query) - expect(summary).toEqual({ ran: 0, skipped: 1 }) + expect(summary).toEqual({ ran: 0, skipped: 1, deferred: 0 }) + }) + + it('treats an index another director already dropped as skipped rather than an error', async () => { + // Every director boots at once on a deploy and all of them send the same DROP INDEX IF EXISTS. + // Only one can win; the losers must not fail their boot over a drop that already happened. + const query = vi.fn(async () => { + throw postgresError('42704') + }) + const summary = await applyPostgresSchema(['DROP INDEX IF EXISTS i'], query) + expect(summary).toEqual({ ran: 0, skipped: 1, deferred: 0 }) + expect(query).toHaveBeenCalledTimes(1) + }) + + it('still propagates 42704 from a statement that is not a DROP IF EXISTS', async () => { + // Keeps the case above narrow: an undefined object anywhere else is a real boot failure. + const query = vi.fn(async () => { + throw postgresError('42704') + }) + await expect( + applyPostgresSchema(['ALTER TABLE t ADD COLUMN IF NOT EXISTS c BIGINT'], query) + ).rejects.toThrow(/42704/) + }) + + it('leaves a deferrable statement unapplied on a lock timeout instead of failing the boot', async () => { + // The crash loop this prevents: 28 directors reach the same DROP INDEX at once on a table + // under continuous write, all of them time out, and every one restarts to re-queue the same + // DDL behind the same writers. + const warned: string[] = [] + vi.spyOn(console, 'warn').mockImplementation((line: string) => { + warned.push(line) + }) + const query = vi.fn(async () => { + throw postgresError('55P03') + }) + const summary = await applyPostgresSchema( + ['-- schema-deferrable: reason\nDROP INDEX IF EXISTS i'], + query + ) + expect(summary).toEqual({ ran: 0, skipped: 0, deferred: 1 }) + expect(query).toHaveBeenCalledTimes(1) + const event = JSON.parse(warned[warned.length - 1] ?? '{}') + expect(event.event).toBe('orca_relay_postgres_schema_object_deferred') + expect(event.code).toBe('55P03') + expect(event.name).toBe('i') + }) + + it('runs the statements after a deferral, rather than abandoning the boot at that point', async () => { + // A deferral is not a failure, so nothing behind it may be skipped: the schema still has + // tables to create, and a boot that stopped here would come up against a partial schema. + const sent: string[] = [] + const query = vi.fn(async (statement: string) => { + sent.push(statement) + if (statement.includes('DROP INDEX')) throw postgresError('55P03') + return undefined + }) + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const summary = await applyPostgresSchema( + ['-- schema-deferrable: reason\nDROP INDEX IF EXISTS i', 'CREATE TABLE IF NOT EXISTS t (id TEXT)'], + query + ) + expect(summary).toEqual({ ran: 1, skipped: 0, deferred: 1 }) + expect(sent).toHaveLength(2) + }) + + it('still fails the boot on a lock timeout for a statement that is not marked deferrable', async () => { + // Keeps the marker meaningful. An unmarked statement retains the old contract: fail once and + // loudly, because retrying parks every writer behind the same queue again. + vi.spyOn(console, 'error').mockImplementation(() => undefined) + const query = vi.fn(async () => { + throw postgresError('55P03') + }) + await expect(applyPostgresSchema(['DROP INDEX IF EXISTS i'], query)).rejects.toMatchObject({ + code: '55P03' + }) + }) + + it('defers only on a lock timeout, not on any other error from a deferrable statement', async () => { + // A deferrable statement is not a statement whose failures stop mattering. A permission error + // is still a boot failure. + const query = vi.fn(async () => { + throw postgresError('42501') + }) + await expect( + applyPostgresSchema(['-- schema-deferrable: reason\nDROP INDEX IF EXISTS i'], query) + ).rejects.toThrow(/42501/) + }) + + it('asks the catalog for a dropped index by name and skips the DROP once it is gone', async () => { + const query = vi.fn(async (_statement: string) => undefined) + const { catalogQuery, asked } = catalogAnswers([]) + const summary = await applyPostgresSchema(['DROP INDEX IF EXISTS i'], query, { catalogQuery }) + // One parameter, the index name: the statement names no table, and the query references no $2. + expect(asked).toEqual([[expect.stringContaining("relkind = 'i'"), 'i']]) + expect(query).not.toHaveBeenCalled() + expect(summary).toEqual({ ran: 0, skipped: 1, deferred: 0 }) + }) + + it('sends the DROP while the index is still there, which is the boot that has to win', async () => { + const query = vi.fn(async (_statement: string) => undefined) + const { catalogQuery } = catalogAnswers([{}]) + const summary = await applyPostgresSchema(['DROP INDEX IF EXISTS i'], query, { catalogQuery }) + expect(query).toHaveBeenCalledTimes(1) + expect(summary).toEqual({ ran: 1, skipped: 0, deferred: 0 }) }) it('propagates an unrelated error without retrying', async () => { @@ -168,7 +271,7 @@ describe('applyPostgresSchema catalog pre-check', () => { expect(asked).toEqual([ [expect.stringContaining('pg_catalog.pg_index'), 'relay_connection_bases', 'relay_bases_active'] ]) - expect(summary).toEqual({ ran: 1, skipped: 1 }) + expect(summary).toEqual({ ran: 1, skipped: 1, deferred: 0 }) }) it('skips an index the catalog reports as invalid rather than rebuilding it', async () => { @@ -206,7 +309,8 @@ describe('applyPostgresSchema catalog pre-check', () => { expect(JSON.parse(logged[logged.length - 1] ?? '{}')).toEqual({ event: 'orca_push_postgres_schema_applied', ran: 1, - skipped: 1 + skipped: 1, + deferred: 0 }) }) @@ -219,7 +323,7 @@ describe('applyPostgresSchema catalog pre-check', () => { [expect.stringContaining('pg_catalog.pg_attribute'), 'relay_control_capabilities', 'idle'] ]) expect(query.mock.calls.map(([sql]) => sql)).toEqual([statement]) - expect(summary).toEqual({ ran: 1, skipped: 0 }) + expect(summary).toEqual({ ran: 1, skipped: 0, deferred: 0 }) }) it('never probes the catalog for a statement that takes no relation lock', async () => { @@ -247,7 +351,7 @@ describe('applyPostgresSchema catalog pre-check', () => { ] ]) expect(query).not.toHaveBeenCalled() - expect(summary).toEqual({ ran: 0, skipped: 1 }) + expect(summary).toEqual({ ran: 0, skipped: 1, deferred: 0 }) }) it('skips a DROP CONSTRAINT IF EXISTS when the constraint is already gone', async () => { @@ -265,7 +369,7 @@ describe('applyPostgresSchema catalog pre-check', () => { { catalogQuery } ) expect(query).not.toHaveBeenCalled() - expect(summary).toEqual({ ran: 0, skipped: 1 }) + expect(summary).toEqual({ ran: 0, skipped: 1, deferred: 0 }) expect(logged).toContainEqual({ event: 'orca_relay_postgres_schema_object_absent', kind: 'constraint', @@ -281,7 +385,7 @@ describe('applyPostgresSchema catalog pre-check', () => { const statement = 'ALTER TABLE t DROP CONSTRAINT IF EXISTS region_check' const summary = await applyPostgresSchema([statement], query, { catalogQuery }) expect(query.mock.calls.map(([sql]) => sql)).toEqual([statement]) - expect(summary).toEqual({ ran: 1, skipped: 0 }) + expect(summary).toEqual({ ran: 1, skipped: 0, deferred: 0 }) }) it('sends an ADD CONSTRAINT the catalog does not name yet', async () => { @@ -290,14 +394,14 @@ describe('applyPostgresSchema catalog pre-check', () => { const statement = 'ALTER TABLE t ADD CONSTRAINT region_valid CHECK (r IN (1))' const summary = await applyPostgresSchema([statement], query, { catalogQuery }) expect(query.mock.calls.map(([sql]) => sql)).toEqual([statement]) - expect(summary).toEqual({ ran: 1, skipped: 0 }) + expect(summary).toEqual({ ran: 1, skipped: 0, deferred: 0 }) }) it('sends every statement when no catalog query is supplied', async () => { const query = vi.fn(async (_statement: string) => undefined) const summary = await applyPostgresSchema([COMMENTED_TABLE, COMMENTED_INDEX], query) expect(query).toHaveBeenCalledTimes(2) - expect(summary).toEqual({ ran: 2, skipped: 0 }) + expect(summary).toEqual({ ran: 2, skipped: 0, deferred: 0 }) }) }) @@ -316,7 +420,7 @@ describe('applyPostgresSchema concurrent creates', () => { }) expect(query).toHaveBeenCalledTimes(1) expect(asked).toHaveLength(2) - expect(summary).toEqual({ ran: 0, skipped: 1 }) + expect(summary).toEqual({ ran: 0, skipped: 1, deferred: 0 }) }) it('still retries when the catalog says the object is not there after all', async () => { @@ -332,7 +436,7 @@ describe('applyPostgresSchema concurrent creates', () => { wait: async () => undefined }) expect(query).toHaveBeenCalledTimes(2) - expect(summary).toEqual({ ran: 1, skipped: 0 }) + expect(summary).toEqual({ ran: 1, skipped: 0, deferred: 0 }) }) it('retries a CREATE TABLE collision without a catalog re-ask, having no target to ask about', async () => { diff --git a/cloud/packages/postgres-schema/src/apply-postgres-schema.ts b/cloud/packages/postgres-schema/src/apply-postgres-schema.ts index 3417d9ccd70..33490ab1147 100644 --- a/cloud/packages/postgres-schema/src/apply-postgres-schema.ts +++ b/cloud/packages/postgres-schema/src/apply-postgres-schema.ts @@ -25,7 +25,7 @@ export type SchemaStartupOptions = { wait?: (delayMs: number) => Promise } -export type SchemaApplySummary = { ran: number; skipped: number } +export type SchemaApplySummary = { ran: number; skipped: number; deferred: number } function retryDelayMs(attempt: number, random: () => number): number { const ceiling = Math.min(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS) @@ -39,6 +39,17 @@ function wait(delayMs: number): Promise { const CREATE_TABLE_IF_NOT_EXISTS = /^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\b/i const CREATE_INDEX_IF_NOT_EXISTS = /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+IF\s+NOT\s+EXISTS\b/i const ALTER_TABLE_ADD_CONSTRAINT = /^ALTER\s+TABLE\s+\S+\s+ADD\s+CONSTRAINT\b/i +const DROP_INDEX_IF_EXISTS = /^DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?IF\s+EXISTS\b/i + +// Marked in the schema text, beside the SQL it applies to, and read from the raw statement because +// classification strips comments. Says: this boot may leave the statement unapplied rather than +// fail. Only sound for a statement that is idempotent AND that nothing this boot goes on to do +// depends on, because the database is then simply as it was and the next boot re-sends it. +const DEFERRABLE = /^\s*--[^\n]*\bschema-deferrable\b/ + +export function schemaDeferrable(statement: string): boolean { + return DEFERRABLE.test(statement) +} // `IF NOT EXISTS` only checks the name before the catalog inserts, so the loser of a concurrent // CREATE can fail on the catalog unique index (23505) or, when the winner has already committed by @@ -70,6 +81,14 @@ function constraintAlreadyApplied(error: unknown, sql: string): boolean { ) } +// `IF EXISTS` resolves the name, then locks; between those two steps another director's drop can +// commit and the loser raises 42704 instead of the notice it would have got a moment later. Every +// director boots at once on a deploy, so without this the losers fail their boot over a drop that +// already happened. +function dropAlreadyApplied(error: unknown, sql: string): boolean { + return DROP_INDEX_IF_EXISTS.test(sql) && (error as { code?: unknown } | null)?.code === '42704' +} + function retryableSchemaError(error: unknown, sql: string): boolean { const value = (error as { code?: unknown; constraint?: unknown } | null) ?? {} return RETRYABLE_SCHEMA_CODES.has(String(value.code)) || concurrentCreateCollision(value, sql) @@ -90,7 +109,7 @@ async function nothingToDo( JSON.stringify({ event: `${eventPrefix}_object_${target.skipWhen}`, kind: target.kind, - table: target.table, + table: target.kind === 'index-by-name' ? undefined : target.table, name: target.name, indisvalid: presence.indisvalid }) @@ -108,7 +127,7 @@ export async function applyPostgresSchema( const random = options.random ?? Math.random const pause = options.wait ?? wait const deadlineAt = now() + (options.retryDeadlineMs ?? DEFAULT_RETRY_DEADLINE_MS) - const summary: SchemaApplySummary = { ran: 0, skipped: 0 } + const summary: SchemaApplySummary = { ran: 0, skipped: 0, deferred: 0 } for (const statement of statements) { // Throws when an index or column statement's target cannot be read, rather than sending it @@ -126,7 +145,7 @@ export async function applyPostgresSchema( summary.ran += 1 break } catch (error) { - if (constraintAlreadyApplied(error, sql)) { + if (constraintAlreadyApplied(error, sql) || dropAlreadyApplied(error, sql)) { summary.skipped += 1 break } @@ -135,6 +154,24 @@ export async function applyPostgresSchema( // this boot lost the queue. Relation locks are granted in queue order, so each retry parks // every writer behind it again for another timeout. Fail once, loudly. if (code === LOCK_NOT_AVAILABLE && !options.retryLockTimeout) { + // A deferrable statement yields the queue instead of crash-looping the instance. Every + // director boots at once on a migration, so a table under continuous write can hand the + // whole fleet a lock timeout on the one statement that has to win once; failing the boot + // for it restarts the instance, which re-queues the same DDL behind the same writers. + if (schemaDeferrable(statement)) { + console.warn( + JSON.stringify({ + event: `${eventPrefix}_object_deferred`, + code, + kind: target?.kind, + name: target?.name, + statement: sql.split('\n')[0], + detail: 'could not take its lock; left unapplied for the next boot to retry' + }) + ) + summary.deferred += 1 + break + } console.error( JSON.stringify({ event: `${eventPrefix}_lock_timeout`, diff --git a/cloud/packages/postgres-schema/src/catalog-object-precheck.ts b/cloud/packages/postgres-schema/src/catalog-object-precheck.ts index 66bc5848608..6f923cb4cd2 100644 --- a/cloud/packages/postgres-schema/src/catalog-object-precheck.ts +++ b/cloud/packages/postgres-schema/src/catalog-object-precheck.ts @@ -26,10 +26,26 @@ WHERE attrelid = to_regclass($1) AND attname = $2 AND attnum > 0 AND NOT attisdr const CONSTRAINT_PRESENT = `SELECT 1 FROM pg_catalog.pg_constraint WHERE conrelid = to_regclass($1) AND conname = $2` +// By name through the search_path, with no table condition, because a DROP INDEX has no table to +// condition on and does not need one: a name that resolves to no visible index is nothing to drop. +// `relkind = 'i'` keeps a same-named table or view from answering for an index. Partitioned indexes +// are 'I', which this deliberately does not match - relay has none, and dropping one is not a +// boot-time operation. +const INDEX_BY_NAME_PRESENT = `SELECT 1 FROM pg_catalog.pg_class c +WHERE c.relname = $1 AND c.relkind = 'i' AND pg_catalog.pg_table_is_visible(c.oid)` + +// reloptions is a text[] of `name=value` pairs, absent entirely while the option is at its +// default. Comparing the whole pair is what makes a changed value re-run: `@>` on a different +// value answers no, and the statement runs and overwrites it. +const RELOPTION_PRESENT = `SELECT 1 FROM pg_catalog.pg_class +WHERE oid = to_regclass($1) AND reloptions @> ARRAY[$2]` + const PRESENCE_SQL = { index: INDEX_PRESENT, column: COLUMN_PRESENT, - constraint: CONSTRAINT_PRESENT + constraint: CONSTRAINT_PRESENT, + reloption: RELOPTION_PRESENT, + 'index-by-name': INDEX_BY_NAME_PRESENT } as const export type SchemaCatalogPresence = { present: boolean; indisvalid: unknown } @@ -42,7 +58,10 @@ export async function catalogObjectPresence( target: SchemaLockTarget ): Promise { const sql = PRESENCE_SQL[target.kind] - const rows = await query(sql, [target.table, target.name]) + // The name-only lookup binds one parameter; every other shape binds the table first. Passing a + // parameter the SQL never references is a bind error, not a harmless extra. + const params = target.kind === 'index-by-name' ? [target.name] : [target.table, target.name] + const rows = await query(sql, params) const row = rows[0] return row ? { present: true, indisvalid: row.indisvalid } : { present: false, indisvalid: undefined } } diff --git a/cloud/packages/postgres-schema/src/index.ts b/cloud/packages/postgres-schema/src/index.ts index af101ade9b7..577d375afd6 100644 --- a/cloud/packages/postgres-schema/src/index.ts +++ b/cloud/packages/postgres-schema/src/index.ts @@ -1,5 +1,6 @@ export { applyPostgresSchema, + schemaDeferrable, type SchemaApplySummary, type SchemaStartupOptions } from './apply-postgres-schema.js' diff --git a/cloud/packages/postgres-schema/src/schema-lock-target.test.ts b/cloud/packages/postgres-schema/src/schema-lock-target.test.ts index ffd04dd81cd..80d2714bd2c 100644 --- a/cloud/packages/postgres-schema/src/schema-lock-target.test.ts +++ b/cloud/packages/postgres-schema/src/schema-lock-target.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { schemaDeferrable } from './apply-postgres-schema.js' import { requireSchemaLockTarget, schemaLockTarget, @@ -417,3 +418,105 @@ describe('dollar-quoted bodies', () => { ) }) }) + +describe('schemaLockTarget storage parameters', () => { + it('reads a storage parameter as a name=value target the catalog can be asked about', () => { + expect(schemaLockTarget('ALTER TABLE t SET (fillfactor = 70)')).toEqual({ + kind: 'reloption', + table: 't', + name: 'fillfactor=70', + skipWhen: 'present' + }) + }) + + it('folds the option name but keeps the value as written, the way pg_class stores the pair', () => { + expect(schemaLockTarget('ALTER TABLE t SET (FillFactor=70)')?.name).toBe('fillfactor=70') + }) + + it('makes a changed value a different target, so it re-runs instead of skipping', () => { + // The failure this prevents: matching on the option name alone would read `fillfactor=100` as + // already satisfying `fillfactor = 70` and skip the statement for the life of the database. + const seventy = schemaLockTarget('ALTER TABLE t SET (fillfactor = 70)') + const eighty = schemaLockTarget('ALTER TABLE t SET (fillfactor = 80)') + expect(seventy?.name).not.toBe(eighty?.name) + }) + + it('refuses a multi-option SET rather than skipping on only the first option', () => { + // Same reason a multi-action ALTER TABLE is refused: skipping on one option would silently + // drop the others for good. + expect(() => + requireSchemaLockTarget('ALTER TABLE t SET (fillfactor = 70, autovacuum_enabled = false)') + ).toThrow(/unparsed_schema_lock_target/) + }) + + it('fails the boot on a SET whose shape it cannot read, rather than sending it unchecked', () => { + // A storage parameter takes a relation lock, so no target means the lock is taken on every + // boot. RESET has no value to compare and is not supported. + expect(() => requireSchemaLockTarget('ALTER TABLE t RESET (fillfactor)')).not.toThrow() + expect(() => requireSchemaLockTarget('ALTER TABLE t SET (fillfactor)')).toThrow( + /unparsed_schema_lock_target/ + ) + }) + + it('takes a relation lock, so the census requires it to carry a target', () => { + expect(takesRelationLock('ALTER TABLE t SET (fillfactor = 70)')).toBe(true) + }) +}) + +describe('schemaLockTarget dropped indexes', () => { + it('resolves a dropped index by name, with no table to name', () => { + expect(schemaLockTarget('DROP INDEX IF EXISTS i')).toEqual({ + kind: 'index-by-name', + name: 'i', + skipWhen: 'absent' + }) + }) + + it('reads CONCURRENTLY as a modifier rather than the index name', () => { + expect(schemaLockTarget('DROP INDEX CONCURRENTLY IF EXISTS i')?.name).toBe('i') + }) + + it('folds an unquoted name and keeps a quoted one, the way relname stores it', () => { + expect(schemaLockTarget('DROP INDEX IF EXISTS MyIndex')?.name).toBe('myindex') + expect(schemaLockTarget('DROP INDEX IF EXISTS "MyIndex"')?.name).toBe('MyIndex') + }) + + it('takes a relation lock, because the index is there on the boot that has to drop it', () => { + expect(takesRelationLock('DROP INDEX IF EXISTS i')).toBe(true) + }) + + it('requires IF EXISTS, so a bare DROP fails the boot instead of running unchecked', () => { + // Same contract as DROP CONSTRAINT: a bare DROP on a missing index is an error the server is + // supposed to raise, and a pre-check that skipped it would swallow that. + expect(() => requireSchemaLockTarget('DROP INDEX i')).toThrow(/unparsed_schema_lock_target/) + }) + + it('refuses a multi-index DROP rather than pre-checking only the first name', () => { + // Skipping on one name would leave the other index in place for the life of the database. + expect(() => requireSchemaLockTarget('DROP INDEX IF EXISTS a, b')).toThrow( + /unparsed_schema_lock_target/ + ) + }) + + it('derives the target through a leading deferrable marker', () => { + // The real shape in relay's schema: the marker is a comment, so classification must see past + // it or the statement would reach the server with no pre-check at all. + const statement = '-- schema-deferrable: reason\nDROP INDEX IF EXISTS i' + expect(sqlWithoutComments(statement)).toBe('DROP INDEX IF EXISTS i') + expect(schemaLockTarget(statement)?.name).toBe('i') + }) +}) + +describe('schemaDeferrable', () => { + it('reads the marker only from a leading comment, never from the SQL body', () => { + // A name or a string containing the word must not make a statement deferrable. + expect(schemaDeferrable('-- schema-deferrable: reason\nDROP INDEX IF EXISTS i')).toBe(true) + expect(schemaDeferrable('DROP INDEX IF EXISTS schema_deferrable')).toBe(false) + expect(schemaDeferrable("CREATE TABLE t (c TEXT DEFAULT 'schema-deferrable')")).toBe(false) + }) + + it('treats an unmarked statement as fatal on a lock timeout, which is the default', () => { + expect(schemaDeferrable('DROP INDEX IF EXISTS i')).toBe(false) + expect(schemaDeferrable('ALTER TABLE t SET (fillfactor = 70)')).toBe(false) + }) +}) diff --git a/cloud/packages/postgres-schema/src/schema-lock-target.ts b/cloud/packages/postgres-schema/src/schema-lock-target.ts index b4b42fc3b5a..8a96dc5fb1d 100644 --- a/cloud/packages/postgres-schema/src/schema-lock-target.ts +++ b/cloud/packages/postgres-schema/src/schema-lock-target.ts @@ -2,14 +2,20 @@ // already exists before the statement joins the lock queue. `table` is kept exactly as the // statement wrote it, schema qualification and quoting included, because it is fed to // `to_regclass`; `name` is the bare identifier the catalog stores in `relname`/`attname`. -export type SchemaLockTarget = { - kind: 'index' | 'column' | 'constraint' - table: string - name: string - // The catalog answer that means this statement has nothing left to do. Creating statements skip - // on present; `DROP CONSTRAINT IF EXISTS` is the inverse, because nothing to drop is done. - skipWhen: 'present' | 'absent' -} +export type SchemaLockTarget = + | { + kind: 'index' | 'column' | 'constraint' | 'reloption' + table: string + name: string + // The catalog answer that means this statement has nothing left to do. Creating statements + // skip on present; `DROP CONSTRAINT IF EXISTS` is the inverse, because nothing to drop is + // done. + skipWhen: 'present' | 'absent' + } + // A `DROP INDEX` names no table, and needs none: an index name that resolves to nothing is + // nothing to drop, whatever table it used to belong to. Resolution is by name through the + // search_path, which is how the DROP itself would resolve it. + | { kind: 'index-by-name'; name: string; skipWhen: 'absent' } // Keywords that sit in an identifier position when the optional clause before them is absent. // Without this, `CREATE UNIQUE INDEX CONCURRENTLY ON t(c)` reads CONCURRENTLY as the index name and @@ -119,10 +125,27 @@ const DROP_CONSTRAINT = new RegExp( 'i' ) +// `IF EXISTS` is required for the same reason it is on DROP CONSTRAINT: a bare `DROP INDEX` on a +// missing index is an error the server is supposed to raise. Without a target the statement throws +// at boot instead, which tells the author to write `IF EXISTS`. +const DROP_INDEX = new RegExp(`^DROP\\s+INDEX\\s+(?:CONCURRENTLY\\s+)?IF\\s+EXISTS\\s+${QUALIFIED}\\s*$`, 'i') + +// One option per statement, and a literal value: the catalog stores reloptions as `name=value` +// text, so the pre-check compares the written pair against that array verbatim. A list of options +// is refused by `hasTopLevelComma` before it reaches here, the same as a multi-action ALTER TABLE. +const SET_RELOPTION = new RegExp( + `^ALTER\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?(?:ONLY\\s+)?${QUALIFIED}\\s+` + + `SET\\s+\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*([A-Za-z0-9_.]+)\\s*\\)\\s*$`, + 'i' +) + // Every statement shape that takes a relation lock before Postgres evaluates its existence test. // `CREATE TABLE IF NOT EXISTS` is absent on purpose: it resolves a name against the schema and // takes no lock on an existing table. -const TAKES_RELATION_LOCK = /^(?:CREATE\s+(?:UNIQUE\s+)?INDEX|ALTER\s+TABLE)\b/i +// `DROP INDEX` is here because it takes ACCESS EXCLUSIVE on the index's table whenever the index is +// actually there, which is every boot until the first one wins. That it takes no lock once the +// index is gone is what the pre-check turns into the steady state, not a reason to omit it. +const TAKES_RELATION_LOCK = /^(?:CREATE\s+(?:UNIQUE\s+)?INDEX|DROP\s+INDEX|ALTER\s+TABLE)\b/i export function takesRelationLock(statement: string): boolean { return TAKES_RELATION_LOCK.test(sqlWithoutComments(statement)) @@ -177,7 +200,9 @@ const MUST_PARSE = [ /^CREATE\s+(?:UNIQUE\s+)?INDEX\b/i, /^ALTER\s+TABLE\b[\s\S]*\bADD\s+COLUMN\b/i, /^ALTER\s+TABLE\b[\s\S]*\bADD\s+CONSTRAINT\b/i, - /^ALTER\s+TABLE\b[\s\S]*\bDROP\s+CONSTRAINT\b/i + /^ALTER\s+TABLE\b[\s\S]*\bDROP\s+CONSTRAINT\b/i, + /^ALTER\s+TABLE\b[\s\S]*\bSET\s+\(/i, + /^DROP\s+INDEX\b/i ] // Derived from the statement itself so a renamed index cannot drift away from its pre-check. @@ -209,6 +234,22 @@ export function schemaLockTarget(statement: string): SchemaLockTarget | undefine skipWhen: 'absent' } } + const droppedIndex = DROP_INDEX.exec(sql) + if (droppedIndex?.[1]) { + return { kind: 'index-by-name', name: catalogName(droppedIndex[1]), skipWhen: 'absent' } + } + const option = SET_RELOPTION.exec(sql) + if (option?.[1] && option[2] && option[3]) { + // Option names are always folded, but the value is stored as written, so only the name goes + // through catalogName. `fillfactor=70` and `fillfactor=80` are different targets, which is + // what makes a changed value re-run rather than skip. + return { + kind: 'reloption', + table: option[1], + name: `${catalogName(option[2])}=${option[3]}`, + skipWhen: 'present' + } + } return undefined } diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index c26d1b4858a..72efa691dbf 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -14,6 +14,10 @@ const { } = require('./packaged-runtime-node-modules.cjs') const { verifyLinuxGlibcFloor } = require('./scripts/verify-linux-glibc-floor.cjs') const { writeMacBuildCompatibility } = require('./scripts/mac-build-compatibility.cjs') +const { + MOBILE_WEB_BUNDLE_DIR, + assertMobileWebBundleBuilt +} = require('./scripts/verify-packaged-mobile-web-bundle.cjs') const { verifyPackagedPluginResources } = require('./scripts/verify-packaged-plugin-resources.cjs') const { verifyPackagedWindowsNodePty @@ -177,6 +181,9 @@ module.exports = { // Why: these repo-only inputs are either bundled into out/ or copied via // extraResources. Shipping them in app.asar bloats the desktop bundle. '!src{,/**/*}', + // Redundant under !src above, kept explicit: the built bundle ships from out/mobile-web via the + // out rules exactly as out/web does, and the source tree must never be mistaken for it. + '!src/mobile-web{,/**/*}', '!config{,/**/*}', '!docs{,/**/*}', '!mobile{,/**/*}', @@ -289,8 +296,11 @@ module.exports = { verifyStaticAppImagePackage(file, arch) } }, - beforePack: (context) => { + // electron-builder calls this with the context alone. The second parameter is the bundle root, + // so a test can point the guard at a scratch bundle instead of needing the repo's out/ built. + beforePack: (context, mobileWebBundleDir = MOBILE_WEB_BUNDLE_DIR) => { assertPackagedNativeVariantsInstalled(context.electronPlatformName, context.arch) + assertMobileWebBundleBuilt(mobileWebBundleDir) }, afterPack: async (context) => { const resourcesDir = diff --git a/config/knip.json b/config/knip.json index 9af0b8d1a74..e5a014b32e1 100644 --- a/config/knip.json +++ b/config/knip.json @@ -16,6 +16,7 @@ "src/main/hang-watchdog/main-thread-hang-watchdog-entry.ts", "src/main/agent-hooks/managed-agent-hook-controls.ts", "src/main/claude-accounts/keychain.ts", + "src/mobile-web/src/bootstrap.ts", "src/renderer/src/main.tsx", "src/renderer/src/popout.tsx", "src/renderer/src/web/main.tsx", 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. diff --git a/config/scripts/build-mobile-web-bundle.mjs b/config/scripts/build-mobile-web-bundle.mjs new file mode 100644 index 00000000000..52957858784 --- /dev/null +++ b/config/scripts/build-mobile-web-bundle.mjs @@ -0,0 +1,230 @@ +import { createHash } from 'node:crypto' +import { realpathSync } from 'node:fs' +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import * as esbuild from 'esbuild' + +const projectDir = fileURLToPath(new URL('../..', import.meta.url)) +const sourceDir = join(projectDir, 'src', 'mobile-web') +const defaultOutDir = join(projectDir, 'out', 'mobile-web') + +export const MOBILE_WEB_BUNDLE_SCHEMA_VERSION = 1 +export const MOBILE_WEB_BUNDLE_ENTRYPOINT = 'index.html' + +const CONTENT_TYPE_BY_EXTENSION = { + css: 'text/css; charset=utf-8', + html: 'text/html; charset=utf-8', + js: 'text/javascript; charset=utf-8', + png: 'image/png' +} + +/** + * Canonical serialization the buildId hashes. Key order is fixed and the list is sorted by path, + * so the id is a pure function of content. Must stay byte-identical to the contract module's + * serializer in src/shared/mobile-web-bundle/. + */ +export function serializeMobileWebBundleAssets(assets) { + return JSON.stringify( + [...assets] + .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) + .map(({ path, sha256, byteLength, contentType }) => ({ + path, + sha256, + byteLength, + contentType + })) + ) +} + +export function computeMobileWebBundleBuildId(assets) { + return createHash('sha256').update(serializeMobileWebBundleAssets(assets), 'utf8').digest('hex') +} + +function sha256Hex(bytes) { + return createHash('sha256').update(bytes).digest('hex') +} + +function contentTypeForExtension(extension) { + const contentType = CONTENT_TYPE_BY_EXTENSION[extension] + if (!contentType) { + throw new Error(`[build-mobile-web-bundle] no content type registered for .${extension}`) + } + return contentType +} + +function readIntegerConstant(source, name) { + const match = new RegExp(`export const ${name} = (\\d+)`).exec(source) + if (!match) { + throw new Error(`[build-mobile-web-bundle] ${name} not found in src/shared/protocol-version.ts`) + } + return Number.parseInt(match[1], 10) +} + +/** + * Parsed rather than imported because protocol-version.ts is TypeScript and this script runs on + * bare node during packaging, before any build output exists. + */ +async function readProtocolWindow() { + const source = await readFile(join(projectDir, 'src', 'shared', 'protocol-version.ts'), 'utf8') + return { + runtimeProtocolVersion: readIntegerConstant(source, 'RUNTIME_PROTOCOL_VERSION'), + // The bundle is a client: the floor it cares about is the oldest host protocol it can talk to. + minCompatibleRuntimeProtocolVersion: readIntegerConstant( + source, + 'MIN_COMPATIBLE_RUNTIME_SERVER_VERSION' + ) + } +} + +async function readDesktopVersion() { + const packageJson = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8')) + if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) { + throw new Error('[build-mobile-web-bundle] root package.json has no version') + } + return packageJson.version +} + +async function transformEntries(protocolWindow, desktopVersion) { + const result = await esbuild.build({ + absWorkingDir: sourceDir, + entryPoints: [join(sourceDir, 'src', 'bootstrap.ts'), join(sourceDir, 'src', 'bootstrap.css')], + bundle: true, + minify: true, + // Virtual: write is false, so outdir only names the emitted files esbuild hands back. + outdir: 'dist', + write: false, + format: 'iife', + target: ['es2022'], + charset: 'utf8', + legalComments: 'none', + // Why no sourcemap and no metafile: both embed absolute paths, which would break reproducibility. + sourcemap: false, + logLevel: 'silent', + define: { + ORCA_MOBILE_WEB_DESKTOP_VERSION: JSON.stringify(desktopVersion), + ORCA_MOBILE_WEB_RUNTIME_PROTOCOL_VERSION: JSON.stringify( + protocolWindow.runtimeProtocolVersion + ), + ORCA_MOBILE_WEB_MIN_COMPATIBLE_RUNTIME_PROTOCOL_VERSION: JSON.stringify( + protocolWindow.minCompatibleRuntimeProtocolVersion + ) + } + }) + const byExtension = new Map() + for (const file of result.outputFiles) { + const extension = file.path.endsWith('.css') ? 'css' : 'js' + byExtension.set(extension, Buffer.from(file.contents)) + } + const script = byExtension.get('js') + const stylesheet = byExtension.get('css') + if (!script || !stylesheet) { + throw new Error('[build-mobile-web-bundle] esbuild did not emit both a script and a stylesheet') + } + return { script, stylesheet } +} + +function hashedAsset(bytes, extension) { + const sha256 = sha256Hex(bytes) + return { + bytes, + path: `assets/${sha256}.${extension}`, + sha256, + byteLength: bytes.byteLength, + contentType: contentTypeForExtension(extension) + } +} + +export async function buildMobileWebBundle({ outDir = defaultOutDir } = {}) { + const [desktopVersion, protocolWindow] = await Promise.all([ + readDesktopVersion(), + readProtocolWindow() + ]) + const { script, stylesheet } = await transformEntries(protocolWindow, desktopVersion) + const mark = await readFile(join(sourceDir, 'src', 'orca-mark.png')) + + const hashed = [ + hashedAsset(script, 'js'), + hashedAsset(stylesheet, 'css'), + hashedAsset(mark, 'png') + ] + const [scriptAsset, stylesheetAsset, markAsset] = hashed + + const template = await readFile(join(sourceDir, MOBILE_WEB_BUNDLE_ENTRYPOINT), 'utf8') + const substitutions = { + __ORCA_BOOTSTRAP_JS__: scriptAsset.path, + __ORCA_BOOTSTRAP_CSS__: stylesheetAsset.path, + __ORCA_MARK_PNG__: markAsset.path + } + let html = template + for (const [token, value] of Object.entries(substitutions)) { + if (!html.includes(token)) { + throw new Error(`[build-mobile-web-bundle] ${MOBILE_WEB_BUNDLE_ENTRYPOINT} lacks ${token}`) + } + html = html.replaceAll(token, value) + } + const indexBytes = Buffer.from(html, 'utf8') + const indexAsset = { + bytes: indexBytes, + path: MOBILE_WEB_BUNDLE_ENTRYPOINT, + sha256: sha256Hex(indexBytes), + byteLength: indexBytes.byteLength, + contentType: contentTypeForExtension('html') + } + + const written = [indexAsset, ...hashed] + const assets = written + .map(({ path, sha256, byteLength, contentType }) => ({ path, sha256, byteLength, contentType })) + .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) + const manifest = { + schemaVersion: MOBILE_WEB_BUNDLE_SCHEMA_VERSION, + buildId: computeMobileWebBundleBuildId(assets), + desktopVersion, + minCompatibleRuntimeProtocolVersion: protocolWindow.minCompatibleRuntimeProtocolVersion, + runtimeProtocolVersion: protocolWindow.runtimeProtocolVersion, + entrypoint: MOBILE_WEB_BUNDLE_ENTRYPOINT, + totalBytes: assets.reduce((total, asset) => total + asset.byteLength, 0), + assets + } + + // Why a full clear: a stale asset left from an earlier build would ship unreferenced inside asar. + await rm(outDir, { recursive: true, force: true }) + await mkdir(join(outDir, 'assets'), { recursive: true }) + for (const asset of written) { + await writeFile(join(outDir, asset.path), asset.bytes) + } + await writeFile(join(outDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8') + return { manifest, outDir } +} + +/** + * Whether this module was run as the entry script. Two ways to get this wrong, both of which end + * with the builder exiting 0 having written nothing: `file://${path}` never matches on Windows, + * where import.meta.url is `file:///C:/...`; and Node resolves symlinks in import.meta.url but not + * in argv[1], so `node /tmp/...` against a /private/tmp realpath compares two different strings. + * Both seams are injectable so win32 and a missing path can be exercised from a posix runner. + */ +export function isDirectInvocation( + moduleUrl, + scriptPath, + { toFileUrl = pathToFileURL, realpath = realpathSync } = {} +) { + if (!scriptPath) { + return false + } + let resolved = scriptPath + try { + resolved = realpath(scriptPath) + } catch { + // A path that cannot be resolved cannot be this module; fall through to the literal compare. + } + return moduleUrl === toFileUrl(resolved).href +} + +if (isDirectInvocation(import.meta.url, process.argv[1])) { + const { manifest, outDir } = await buildMobileWebBundle() + console.log( + `[build-mobile-web-bundle] OK — ${String(manifest.assets.length)} asset(s), ` + + `${String(manifest.totalBytes)} bytes, buildId ${manifest.buildId} -> ${outDir}` + ) +} diff --git a/config/scripts/build-mobile-web-bundle.test.mjs b/config/scripts/build-mobile-web-bundle.test.mjs new file mode 100644 index 00000000000..ca2f74bd1d0 --- /dev/null +++ b/config/scripts/build-mobile-web-bundle.test.mjs @@ -0,0 +1,261 @@ +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { + buildMobileWebBundle, + computeMobileWebBundleBuildId, + isDirectInvocation, + serializeMobileWebBundleAssets +} from './build-mobile-web-bundle.mjs' +import { + MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS, + MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES, + assertNoCarriageReturnsInSource +} from './verify-mobile-web-bundle.mjs' + +async function buildIntoScratch() { + const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-build-')) + const bundleDir = join(scratch, 'mobile-web') + const { manifest } = await buildMobileWebBundle({ outDir: bundleDir }) + return { scratch, bundleDir, manifest } +} + +describe('buildMobileWebBundle', () => { + it('emits a content-addressed bundle whose only stable name is the entrypoint', async () => { + const { scratch, bundleDir, manifest } = await buildIntoScratch() + try { + const root = await readdir(bundleDir) + expect(root.sort()).toEqual(['assets', 'index.html', 'manifest.json']) + for (const name of await readdir(join(bundleDir, 'assets'))) { + const [digest, extension] = name.split('.') + expect(digest).toMatch(/^[0-9a-f]{64}$/) + const bytes = await readFile(join(bundleDir, 'assets', name)) + expect(createHash('sha256').update(bytes).digest('hex')).toBe(digest) + expect(extension).toMatch(/^(js|css|png)$/) + } + const html = await readFile(join(bundleDir, 'index.html'), 'utf8') + for (const asset of manifest.assets) { + if (asset.path !== 'index.html') { + expect(html).toContain(asset.path) + } + } + expect(html).not.toContain('__ORCA_') + } finally { + await rm(scratch, { recursive: true, force: true }) + } + }) + + it('carries every manifest field the Phase A contract names', async () => { + const { scratch, manifest } = await buildIntoScratch() + try { + expect(Object.keys(manifest)).toEqual([ + 'schemaVersion', + 'buildId', + 'desktopVersion', + 'minCompatibleRuntimeProtocolVersion', + 'runtimeProtocolVersion', + 'entrypoint', + 'totalBytes', + 'assets' + ]) + expect(manifest.schemaVersion).toBe(1) + expect(manifest.entrypoint).toBe('index.html') + const packageJson = JSON.parse( + await readFile(new URL('../../package.json', import.meta.url), 'utf8') + ) + expect(manifest.desktopVersion).toBe(packageJson.version) + const protocolSource = await readFile( + new URL('../../src/shared/protocol-version.ts', import.meta.url), + 'utf8' + ) + expect(protocolSource).toContain( + `export const RUNTIME_PROTOCOL_VERSION = ${String(manifest.runtimeProtocolVersion)}` + ) + expect(protocolSource).toContain( + `export const MIN_COMPATIBLE_RUNTIME_SERVER_VERSION = ${String(manifest.minCompatibleRuntimeProtocolVersion)}` + ) + expect(manifest.totalBytes).toBe( + manifest.assets.reduce((total, asset) => total + asset.byteLength, 0) + ) + } finally { + await rm(scratch, { recursive: true, force: true }) + } + }) + + it('produces the same buildId from two independent builds', async () => { + const first = await buildIntoScratch() + const second = await buildIntoScratch() + try { + expect(second.manifest.buildId).toBe(first.manifest.buildId) + expect(second.manifest).toEqual(first.manifest) + } finally { + await rm(first.scratch, { recursive: true, force: true }) + await rm(second.scratch, { recursive: true, force: true }) + } + }) + + it('embeds no absolute path from the machine that built it', async () => { + const { scratch, bundleDir } = await buildIntoScratch() + try { + const names = [ + 'index.html', + 'manifest.json', + ...(await readdir(join(bundleDir, 'assets'))).map((name) => join('assets', name)) + ] + for (const name of names) { + const text = (await readFile(join(bundleDir, name))).toString('latin1') + expect(text).not.toContain(scratch) + expect(text).not.toContain(process.cwd()) + } + } finally { + await rm(scratch, { recursive: true, force: true }) + } + }) + + it('stays inside the Phase A budget', async () => { + const { scratch, manifest } = await buildIntoScratch() + try { + expect(manifest.assets.length).toBeLessThanOrEqual(MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS) + expect(manifest.totalBytes).toBeLessThanOrEqual(MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES) + } finally { + await rm(scratch, { recursive: true, force: true }) + } + }) +}) + +describe('computeMobileWebBundleBuildId', () => { + const assets = [ + { path: 'index.html', sha256: 'a'.repeat(64), byteLength: 3, contentType: 'text/html' }, + { path: 'assets/b.js', sha256: 'b'.repeat(64), byteLength: 5, contentType: 'text/javascript' } + ] + + it('sorts by path, so input order cannot change the id', () => { + expect(computeMobileWebBundleBuildId(assets.toReversed())).toBe( + computeMobileWebBundleBuildId(assets) + ) + }) + + it('serializes a fixed key order regardless of the input object key order', () => { + const reordered = assets.map(({ contentType, byteLength, sha256, path }) => ({ + contentType, + byteLength, + sha256, + path + })) + expect(serializeMobileWebBundleAssets(reordered)).toBe(serializeMobileWebBundleAssets(assets)) + }) + + it('changes when any hashed field changes', () => { + const baseline = computeMobileWebBundleBuildId(assets) + for (const field of ['sha256', 'byteLength', 'contentType', 'path']) { + const mutated = assets.map((asset, index) => + index === 0 ? { ...asset, [field]: field === 'byteLength' ? 4 : `${asset[field]}x` } : asset + ) + expect(computeMobileWebBundleBuildId(mutated)).not.toBe(baseline) + } + }) +}) + +describe('isDirectInvocation', () => { + const thisFile = import.meta.filename + + it('matches the path this module was loaded from', () => { + expect(isDirectInvocation(import.meta.url, thisFile)).toBe(true) + }) + + it('does not match a different script', () => { + expect(isDirectInvocation(import.meta.url, join(thisFile, '..', 'other.mjs'))).toBe(false) + }) + + it('tolerates an absent argv[1]', () => { + expect(isDirectInvocation(import.meta.url, undefined)).toBe(false) + expect(isDirectInvocation(import.meta.url, '')).toBe(false) + }) + + // Why an injected converter: a win32 path cannot be exercised through node:url's pathToFileURL + // on a posix runner, and CI is ubuntu. + const toWin32FileUrl = (windowsPath) => new URL(`file:///${windowsPath.replaceAll('\\', '/')}`) + + it('matches a Windows entry path, which the file:// template form never does', () => { + const scriptPath = 'C:\\orca\\config\\scripts\\build-mobile-web-bundle.mjs' + const moduleUrl = 'file:///C:/orca/config/scripts/build-mobile-web-bundle.mjs' + const keepAsIs = (path) => path + expect( + isDirectInvocation(moduleUrl, scriptPath, { + toFileUrl: toWin32FileUrl, + realpath: keepAsIs + }) + ).toBe(true) + // The regression this guards: `file://${argv[1]}` yields file://C:\orca\... on Windows, + // so the builder exited 0 having written nothing and packaging failed downstream. + expect(`file://${scriptPath}`).not.toBe(moduleUrl) + }) + + it('is not written with the file:// template form', async () => { + const source = await readFile(new URL('./build-mobile-web-bundle.mjs', import.meta.url), 'utf8') + expect(source).not.toMatch(/file:\/\/\$\{process\.argv\[1\]\}/) + expect(source).toContain('pathToFileURL') + }) +}) + +describe('mobile web source line endings', () => { + it('accepts the committed source tree', async () => { + await expect(assertNoCarriageReturnsInSource()).resolves.toBeUndefined() + }) + + it('rejects a CRLF source file, because CRLF changes every asset hash and the buildId', async () => { + const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-eol-')) + try { + await writeFile(join(scratch, 'bootstrap.ts'), 'const a = 1\r\nconst b = 2\r\n', 'utf8') + await expect(assertNoCarriageReturnsInSource(scratch)).rejects.toThrow( + /CRLF in mobile web source/ + ) + } finally { + await rm(scratch, { recursive: true, force: true }) + } + }) + + it('pins eol=lf for every committed text source and -text for the binary', () => { + const files = execFileSync('git', ['ls-files', 'src/mobile-web'], { encoding: 'utf8' }) + .split('\n') + .filter(Boolean) + expect(files.length).toBeGreaterThanOrEqual(4) + for (const file of files) { + const attributes = execFileSync('git', ['check-attr', 'text', 'eol', '--', file], { + encoding: 'utf8' + }) + if (file.endsWith('.png')) { + expect(attributes).toContain('text: unset') + } else { + expect(attributes).toContain('eol: lf') + } + } + }) +}) + +describe('running the builder through a symlink', () => { + // Node resolves symlinks in import.meta.url but not in argv[1]. Before the guard realpath'd the + // entry path, `node /tmp/` compared /tmp against /private/tmp and the builder exited 0 + // having written nothing — a green packaging job with no bundle in it. + it('still recognises the entry module', async () => { + const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-link-')) + try { + const builderUrl = new URL('./build-mobile-web-bundle.mjs', import.meta.url).href + const real = join(scratch, 'entry.mjs') + await writeFile( + real, + `import { isDirectInvocation } from ${JSON.stringify(builderUrl)}\n` + + 'process.stdout.write(String(isDirectInvocation(import.meta.url, process.argv[1])))\n', + 'utf8' + ) + const link = join(scratch, 'entry-link.mjs') + await symlink(real, link) + expect(execFileSync(process.execPath, [link], { encoding: 'utf8' })).toBe('true') + } finally { + await rm(scratch, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index b7d5f16400b..f88967e5132 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -3,7 +3,8 @@ import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { describe, expect, it } from 'vitest' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { buildMobileWebBundle } from './build-mobile-web-bundle.mjs' const REPO_ROOT = join(import.meta.dirname, '..', '..') const SRC_MAIN_DIR = join(REPO_ROOT, 'src', 'main') @@ -442,8 +443,23 @@ describe('arch-aware packaging guard', () => { const OTHER_ARCH_NAME = process.arch === 'arm64' ? 'x64' : 'arm64' const SHERPA_PLATFORM = process.platform === 'win32' ? 'win' : process.platform const otherSherpa = `sherpa-onnx-${SHERPA_PLATFORM}-${OTHER_ARCH_NAME}` + + // beforePack also hash-verifies the mobile web bundle, which the unit-test job never builds. + // Point it at a real bundle built into a temp dir: these tests are about the native-variant + // guard, and the bundle guard has its own suite. + let scratch + let bundleDir + beforeAll(async () => { + scratch = await mkdtemp(join(tmpdir(), 'orca-electron-builder-guard-')) + bundleDir = join(scratch, 'mobile-web') + await buildMobileWebBundle({ outDir: bundleDir }) + }) + afterAll(async () => { + await rm(scratch, { recursive: true, force: true }) + }) + const packHost = (arch) => - electronBuilderConfig.beforePack({ electronPlatformName: process.platform, arch }) + electronBuilderConfig.beforePack({ electronPlatformName: process.platform, arch }, bundleDir) it('allows packaging the host platform and architecture', () => { expect(() => packHost(HOST_ARCH)).not.toThrow() @@ -471,7 +487,7 @@ describe('arch-aware packaging guard', () => { (resource) => resource.to === join('node_modules', '@vscode', 'windows-process-tree') ) const packWindows = () => - electronBuilderConfig.beforePack({ electronPlatformName: 'win32', arch: 1 }) + electronBuilderConfig.beforePack({ electronPlatformName: 'win32', arch: 1 }, bundleDir) if (process.platform === 'win32' || windowsAddon) { expect(packWindows).not.toThrow() } else { diff --git a/config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs b/config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs new file mode 100644 index 00000000000..203bea1d87e --- /dev/null +++ b/config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs @@ -0,0 +1,159 @@ +import { readFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { parseDocument } from 'yaml' + +const workflowsDir = fileURLToPath(new URL('../../.github/workflows', import.meta.url)) + +// Every script whose chain reaches build:mobile-web. build:unpack -> build -> build:desktop, and +// build:mac/linux/win each call build:desktop, so all of them produce out/mobile-web. The chain +// itself is not an assumption here: 'the build scripts' below resolves each one for real. +const BUNDLE_PRODUCING_SCRIPTS = [ + 'build', + 'build:desktop', + 'build:release', + 'build:release:parallel', + 'build:unpack', + 'build:mobile-web', + 'build:mac', + 'build:mac:release', + 'build:linux', + 'build:win' +] + +const BUNDLE_PRODUCER = new RegExp( + `pnpm (?:run )?(?:${BUNDLE_PRODUCING_SCRIPTS.join('|')})(?=$|[\\s'"&|;])`, + 'm' +) + +const packageScripts = JSON.parse( + readFileSync(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf8') +).scripts + +const SCRIPT_INVOCATION = /pnpm (?:run )?([\w:-]+)(?=$|[\s'"&|;])/g + +/** Whether `pnpm run ` eventually runs build:mobile-web. */ +function reachesBundleBuild(name, seen = new Set()) { + if (name === 'build:mobile-web') { + return true + } + if (seen.has(name)) { + return false + } + seen.add(name) + const body = packageScripts[name] + if (typeof body !== 'string') { + return false + } + return [...body.matchAll(SCRIPT_INVOCATION)].some((match) => reachesBundleBuild(match[1], seen)) +} + +/** + * Whether `pnpm run ` eventually runs electron-builder without --prepackaged, i.e. runs + * beforePack. A workflow job that packs through such a script is a packaging job even though the + * literal electron-builder line lives in package.json (daemon-relocation-spike's build:unpack). + */ +function reachesElectronBuilder(name, seen = new Set()) { + if (seen.has(name)) { + return false + } + seen.add(name) + const body = packageScripts[name] + if (typeof body !== 'string') { + return false + } + if (packsWithBeforePack(body)) { + return true + } + return [...body.matchAll(SCRIPT_INVOCATION)].some((match) => + reachesElectronBuilder(match[1], seen) + ) +} + +/** Whether text invokes electron-builder in a way that reaches beforePack. */ +function packsWithBeforePack(text) { + const invocations = [...text.matchAll(/[^\n]*electron-builder --config[^\n]*/g)].map( + (match) => match[0] + ) + // --prepackaged short-circuits doPack before emitBeforePack, so those jobs never run the guard. + return ( + invocations.length > 0 && + !invocations.every((invocation) => invocation.includes('--prepackaged')) + ) +} + +// Every job that packs an app and therefore runs beforePack. Listed so that a new packaging +// workflow has to be added here deliberately, with its bundle step, rather than slipping in. +const EXPECTED_PACKAGING_JOBS = [ + 'adhoc-mac-build.yml build-adhoc-mac', + 'daemon-relocation-spike.yml spike', + 'daily-mac-build.yml build-daily-mac', + 'dev-channel-win-build.yml build-win', + 'hourly-mac-build.yml build-hourly-mac', + 'pr.yml package', + 'pr.yml package_windows', + 'release-cut.yml build', + 'release-mac-build.yml build-mac', + 'win-crash-survival-e2e.yml crash-survival', + 'win-update-survival-e2e.yml survival', + 'windows-signing-rehearsal.yml rehearse' +] + +/** + * Raw source text per job, sliced by the parsed job boundaries. Why not yaml.stringify(job): + * re-serializing folds long lines, and the fold in dev-channel-win-build's build-win landed + * between `electron-builder` and `--config`, hiding a whole packaging job from this census. + */ +function packagingJobs() { + const jobs = [] + for (const file of readdirSync(workflowsDir).filter((name) => name.endsWith('.yml'))) { + const source = readFileSync(join(workflowsDir, file), 'utf8') + const jobsNode = parseDocument(source).get('jobs', true) + const items = jobsNode?.items ?? [] + for (const [index, pair] of items.entries()) { + const end = index + 1 < items.length ? items[index + 1].key.range[0] : jobsNode.range[2] + const text = source.slice(pair.key.range[0], end) + const packsViaScript = [...text.matchAll(SCRIPT_INVOCATION)].some((match) => + reachesElectronBuilder(match[1]) + ) + if (!packsWithBeforePack(text) && !packsViaScript) { + continue + } + jobs.push({ label: `${file} ${String(pair.key.value)}`, text }) + } + } + return jobs +} + +describe('mobile web bundle packaging coverage', () => { + it('finds every packaging job', () => { + // A rename or a restructure that shrank this list would make every assertion below vacuous. + const labels = packagingJobs().map((job) => job.label) + expect(labels.length).toBeGreaterThanOrEqual(EXPECTED_PACKAGING_JOBS.length) + expect(labels.toSorted()).toEqual(EXPECTED_PACKAGING_JOBS.toSorted()) + }) + + it.each(packagingJobs().map((job) => [job.label, job]))( + 'produces out/mobile-web before electron-builder packs: %s', + (_label, job) => { + // Job granularity, not step ordering: the failure this exists for is a job that never builds + // the bundle at all, which is what beforePack turns into a hard packaging failure. + expect(job.text).toMatch(BUNDLE_PRODUCER) + } + ) +}) + +describe('the build scripts the census trusts', () => { + // The census only checks that a packaging job invokes one of these. If a chain stopped calling + // build:mobile-web, every job would still look covered while packaging failed at beforePack. + it.each(BUNDLE_PRODUCING_SCRIPTS)('%s runs build:mobile-web', (name) => { + expect(packageScripts[name]).toBeTypeOf('string') + expect(reachesBundleBuild(name)).toBe(true) + }) + + it('pr.yml package builds the bundle by hand, because it never calls build:release', () => { + const source = readFileSync(join(workflowsDir, 'pr.yml'), 'utf8') + expect(source).toMatch(/- name: Build mobile web bundle\n\s+run: pnpm run build:mobile-web\n/) + }) +}) 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/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/config/scripts/run-multi-workspace-typing-bench.mjs b/config/scripts/run-multi-workspace-typing-bench.mjs index d495fed25ec..2aae129c9b2 100644 --- a/config/scripts/run-multi-workspace-typing-bench.mjs +++ b/config/scripts/run-multi-workspace-typing-bench.mjs @@ -40,6 +40,8 @@ const knobByFlag = { '--metadata-status': 'ORCA_TYPING_BENCH_METADATA_STATUS', '--metadata-titles': 'ORCA_TYPING_BENCH_METADATA_TITLES', '--instrumentation': 'ORCA_TYPING_BENCH_INSTRUMENTATION', + '--graph-probe': 'ORCA_TYPING_BENCH_GRAPH_PROBE', + '--cpu-throttle': 'ORCA_TYPING_BENCH_CPU_THROTTLE', '--label': 'ORCA_TYPING_BENCH_LABEL' } 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/config/scripts/run-typecheck-projects-in-parallel.mjs b/config/scripts/run-typecheck-projects-in-parallel.mjs index 13047989db5..09b0a8e1d1f 100644 --- a/config/scripts/run-typecheck-projects-in-parallel.mjs +++ b/config/scripts/run-typecheck-projects-in-parallel.mjs @@ -2,9 +2,14 @@ import { spawn } from 'node:child_process' import { availableParallelism } from 'node:os' import { fileURLToPath } from 'node:url' -// The three projects overlap heavily in src/shared but have no build dependency on +// These projects overlap heavily in src/shared but have no build dependency on // each other, so tsc can check them concurrently instead of in a `&&` chain. -const projects = ['tsconfig.node.json', 'tsconfig.tc.cli.json', 'tsconfig.tc.web.json'] +const projects = [ + 'tsconfig.node.json', + 'tsconfig.tc.cli.json', + 'tsconfig.tc.web.json', + 'tsconfig.mobile-web.json' +] const repoRoot = fileURLToPath(new URL('../..', import.meta.url)) const tsc = fileURLToPath(new URL('../../node_modules/typescript/bin/tsc', import.meta.url)) diff --git a/config/scripts/verify-mobile-web-bundle.mjs b/config/scripts/verify-mobile-web-bundle.mjs new file mode 100644 index 00000000000..0fedaac11c1 --- /dev/null +++ b/config/scripts/verify-mobile-web-bundle.mjs @@ -0,0 +1,117 @@ +import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { buildMobileWebBundle, isDirectInvocation } from './build-mobile-web-bundle.mjs' +import { assertMobileWebBundleBuilt } from './verify-packaged-mobile-web-bundle.cjs' + +const projectDir = fileURLToPath(new URL('../..', import.meta.url)) +const bundleDir = join(projectDir, 'out', 'mobile-web') +const sourceDir = join(projectDir, 'src', 'mobile-web') + +// Phase A budget, not the contract ceiling: a bootstrap page past a quarter-megabyte has stopped +// being a bootstrap. Phase C raises these deliberately. +export const MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS = 16 +export const MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES = 256 * 1024 + +class VerificationError extends Error {} + +function fail(message) { + throw new VerificationError(message) +} + +async function buildIntoScratch() { + const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-verify-')) + try { + const { manifest } = await buildMobileWebBundle({ outDir: join(scratch, 'mobile-web') }) + return manifest + } finally { + await rm(scratch, { recursive: true, force: true }) + } +} + +async function listSourceFiles(directory) { + const entries = await readdir(directory, { withFileTypes: true }) + const files = [] + for (const entry of entries) { + const entryPath = join(directory, entry.name) + if (entry.isDirectory()) { + files.push(...(await listSourceFiles(entryPath))) + } else if (entry.isFile()) { + files.push(entryPath) + } + } + return files.sort() +} + +/** + * A CRLF checkout changes the bytes of every text source, which changes every asset hash and so + * the buildId. .gitattributes pins eol=lf; this is what notices when that pin stops working. + */ +export async function assertNoCarriageReturnsInSource(directory = sourceDir) { + const offenders = [] + for (const file of await listSourceFiles(directory)) { + // Binary assets are pinned -text and may legitimately contain 0x0d. + if (file.endsWith('.png')) { + continue + } + if ((await readFile(file)).includes(0x0d)) { + offenders.push(file.slice(directory.length + 1)) + } + } + if (offenders.length > 0) { + fail( + `CRLF in mobile web source, which would change every asset hash and the buildId: ` + + `${offenders.join(', ')}. Check the .gitattributes eol=lf pin for src/mobile-web.` + ) + } +} + +export async function verifyMobileWebBundle() { + await assertNoCarriageReturnsInSource() + + // The packaging guard owns manifest integrity (safe paths, recomputed buildId, totalBytes, hashes, + // no stray files); a manifest edited after the build fails here exactly as it would at beforePack. + const manifest = assertMobileWebBundleBuilt(bundleDir) + + if (manifest.assets.length > MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS) { + fail( + `bundle has ${String(manifest.assets.length)} assets, over the Phase A budget of ` + + `${String(MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS)}` + ) + } + if (manifest.totalBytes > MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES) { + fail( + `bundle is ${String(manifest.totalBytes)} bytes, over the Phase A budget of ` + + `${String(MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES)}` + ) + } + + // Two fresh builds into scratch dirs: a timestamp, an absolute path, or an unstable ordering + // anywhere in the pipeline shows up here as a buildId mismatch rather than as a phone cache miss. + const first = await buildIntoScratch() + const second = await buildIntoScratch() + if (first.buildId !== second.buildId) { + fail(`buildId is not reproducible: ${first.buildId} then ${second.buildId}`) + } + if (first.buildId !== manifest.buildId) { + fail( + `${bundleDir} is stale: it carries buildId ${manifest.buildId}, a fresh build produces ${first.buildId}` + ) + } + return manifest +} + +if (isDirectInvocation(import.meta.url, process.argv[1])) { + try { + const manifest = await verifyMobileWebBundle() + console.log( + `[verify-mobile-web-bundle] OK — ${String(manifest.assets.length)} asset(s), ` + + `${String(manifest.totalBytes)}/${String(MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES)} bytes, ` + + `reproducible buildId ${manifest.buildId}` + ) + } catch (error) { + console.error(`[verify-mobile-web-bundle] ${error.message}`) + process.exit(1) + } +} diff --git a/config/scripts/verify-packaged-mobile-web-bundle.cjs b/config/scripts/verify-packaged-mobile-web-bundle.cjs new file mode 100644 index 00000000000..c521882691d --- /dev/null +++ b/config/scripts/verify-packaged-mobile-web-bundle.cjs @@ -0,0 +1,193 @@ +const { createHash } = require('node:crypto') +const { readFileSync, readdirSync, statSync } = require('node:fs') +const { join, resolve } = require('node:path') + +const projectDir = resolve(__dirname, '..', '..') +const MOBILE_WEB_BUNDLE_DIR = join(projectDir, 'out', 'mobile-web') +const REMEDY = 'Run pnpm build:mobile-web (build:desktop and build:release already do).' +const ENTRYPOINT = 'index.html' +const SHA256_PATTERN = /^[0-9a-f]{64}$/ + +function failure(message) { + return new Error(`[verify-packaged-mobile-web-bundle] ${message}`) +} + +function assertSafeRelativePath(path) { + if (typeof path !== 'string' || path.length === 0) { + throw failure('manifest asset has a missing or empty path') + } + const segments = path.split('/') + if ( + path.includes('\\') || + path.startsWith('/') || + /^[a-zA-Z]:/.test(path) || + segments.some((segment) => segment === '' || segment === '.' || segment === '..') + ) { + throw failure(`manifest asset path is not a safe relative path: ${path}`) + } +} + +function assertInteger(value, field) { + if (!Number.isSafeInteger(value) || value < 0) { + throw failure(`manifest field ${field} is not a non-negative integer: ${String(value)}`) + } +} + +/** + * Canonical serialization of the asset list. Must stay byte-identical to + * serializeMobileWebBundleAssets in config/scripts/build-mobile-web-bundle.mjs; a divergence here + * would reject every honest bundle, so the two move together. + */ +function serializeAssets(assets) { + return JSON.stringify( + [...assets] + .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) + .map(({ path, sha256, byteLength, contentType }) => ({ + path, + sha256, + byteLength, + contentType + })) + ) +} + +function parseManifest(bundleDir) { + const manifestPath = join(bundleDir, 'manifest.json') + let raw + try { + raw = readFileSync(manifestPath, 'utf8') + } catch (error) { + throw failure( + `no bundle manifest at ${manifestPath} (${error.code ?? error.message}). ${REMEDY}` + ) + } + let manifest + try { + manifest = JSON.parse(raw) + } catch (error) { + throw failure(`${manifestPath} is not valid JSON: ${error.message}. ${REMEDY}`) + } + if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) { + throw failure(`${manifestPath} is not a JSON object. ${REMEDY}`) + } + if (manifest.schemaVersion !== 1) { + throw failure(`unsupported manifest schemaVersion: ${String(manifest.schemaVersion)}`) + } + if (typeof manifest.buildId !== 'string' || !SHA256_PATTERN.test(manifest.buildId)) { + throw failure(`manifest buildId is not a sha256 digest: ${String(manifest.buildId)}`) + } + if (typeof manifest.desktopVersion !== 'string' || manifest.desktopVersion.length === 0) { + throw failure('manifest desktopVersion is missing') + } + assertInteger(manifest.minCompatibleRuntimeProtocolVersion, 'minCompatibleRuntimeProtocolVersion') + assertInteger(manifest.runtimeProtocolVersion, 'runtimeProtocolVersion') + assertInteger(manifest.totalBytes, 'totalBytes') + if (manifest.entrypoint !== ENTRYPOINT) { + throw failure(`manifest entrypoint must be ${ENTRYPOINT}, got ${String(manifest.entrypoint)}`) + } + if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) { + throw failure('manifest lists no assets') + } + for (const asset of manifest.assets) { + if (typeof asset !== 'object' || asset === null) { + throw failure('manifest asset entry is not an object') + } + assertSafeRelativePath(asset.path) + if (typeof asset.sha256 !== 'string' || !SHA256_PATTERN.test(asset.sha256)) { + throw failure(`manifest asset ${asset.path} has no sha256 digest`) + } + assertInteger(asset.byteLength, `assets[${asset.path}].byteLength`) + if (typeof asset.contentType !== 'string' || asset.contentType.length === 0) { + throw failure(`manifest asset ${asset.path} has no contentType`) + } + } + if (!manifest.assets.some((asset) => asset.path === manifest.entrypoint)) { + throw failure(`manifest entrypoint ${manifest.entrypoint} is not one of its assets`) + } + const declaredTotal = manifest.assets.reduce((total, asset) => total + asset.byteLength, 0) + if (declaredTotal !== manifest.totalBytes) { + throw failure( + `manifest totalBytes is ${String(manifest.totalBytes)}, its assets sum to ${String(declaredTotal)}` + ) + } + const recomputed = createHash('sha256') + .update(serializeAssets(manifest.assets), 'utf8') + .digest('hex') + if (recomputed !== manifest.buildId) { + throw failure( + `manifest buildId ${manifest.buildId} does not match its asset list (expected ${recomputed}). ${REMEDY}` + ) + } + return manifest +} + +/** Every file under the bundle directory, as a manifest-shaped relative path. */ +function listBundleFiles(directory, prefix = '') { + const found = [] + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const relativePath = prefix === '' ? entry.name : `${prefix}/${entry.name}` + if (entry.isDirectory()) { + found.push(...listBundleFiles(join(directory, entry.name), relativePath)) + } else { + found.push(relativePath) + } + } + return found +} + +/** + * Nothing in the bundle directory may be unaccounted for. An asset dropped from the manifest but + * left on disk by an interrupted build ships inside asar, unreachable and unverified, and grows + * the installer; content-addressed names mean stale copies never get overwritten. + */ +function assertNoUnlistedFiles(bundleDir, manifest) { + const listed = new Set(['manifest.json', ...manifest.assets.map((asset) => asset.path)]) + const strays = listBundleFiles(bundleDir).filter((path) => !listed.has(path)) + if (strays.length > 0) { + throw failure( + `${bundleDir} holds ${String(strays.length)} file(s) the manifest does not list: ` + + `${strays.sort().join(', ')}. ${REMEDY}` + ) + } +} + +/** + * Packaging guard: electron-builder only warns about a missing input, so without this a release + * would ship an app that advertises the bundle capability and then errors on every request. The + * hash check, not the existence check, is what catches a half-written or stale out/. + */ +function assertMobileWebBundleBuilt(bundleDir = MOBILE_WEB_BUNDLE_DIR) { + const manifest = parseManifest(bundleDir) + assertNoUnlistedFiles(bundleDir, manifest) + for (const asset of manifest.assets) { + const assetPath = join(bundleDir, asset.path) + let size + try { + size = statSync(assetPath).size + } catch (error) { + throw failure( + `manifest lists ${asset.path}, which is missing from ${bundleDir} (${error.code ?? error.message}). ${REMEDY}` + ) + } + if (size !== asset.byteLength) { + throw failure( + `${asset.path} is ${String(size)} bytes on disk, manifest says ${String(asset.byteLength)}. ${REMEDY}` + ) + } + const sha256 = createHash('sha256').update(readFileSync(assetPath)).digest('hex') + if (sha256 !== asset.sha256) { + throw failure( + `${asset.path} hashes to ${sha256} on disk, manifest says ${asset.sha256}. ${REMEDY}` + ) + } + } + console.log( + `[verify-packaged-mobile-web-bundle] OK — buildId ${manifest.buildId}, ` + + `${String(manifest.assets.length)} asset(s), ${String(manifest.totalBytes)} bytes` + ) + return manifest +} + +// serializeAssets is exported for the parity test that pins it against the builder's and the +// contract's serializers; nothing in packaging calls it from outside this module. +module.exports = { MOBILE_WEB_BUNDLE_DIR, assertMobileWebBundleBuilt, serializeAssets } diff --git a/config/scripts/verify-packaged-mobile-web-bundle.test.mjs b/config/scripts/verify-packaged-mobile-web-bundle.test.mjs new file mode 100644 index 00000000000..40acb8fb144 --- /dev/null +++ b/config/scripts/verify-packaged-mobile-web-bundle.test.mjs @@ -0,0 +1,214 @@ +import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { buildMobileWebBundle } from './build-mobile-web-bundle.mjs' + +const require = createRequire(import.meta.url) +const { + MOBILE_WEB_BUNDLE_DIR, + assertMobileWebBundleBuilt +} = require('./verify-packaged-mobile-web-bundle.cjs') +const electronBuilderConfig = require('../electron-builder.config.cjs') +const REPO_ROOT = join(import.meta.dirname, '..', '..') + +async function withBundle(run) { + const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-guard-')) + const bundleDir = join(scratch, 'mobile-web') + try { + const { manifest } = await buildMobileWebBundle({ outDir: bundleDir }) + await run({ bundleDir, manifest }) + } finally { + await rm(scratch, { recursive: true, force: true }) + } +} + +async function rewriteManifest(bundleDir, mutate) { + const manifestPath = join(bundleDir, 'manifest.json') + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) + mutate(manifest) + await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8') +} + +describe('assertMobileWebBundleBuilt', () => { + beforeEach(() => { + vi.spyOn(console, 'log').mockImplementation(() => {}) + }) + afterEach(() => { + vi.restoreAllMocks() + }) + + it('accepts a freshly built bundle', async () => { + await withBundle(({ bundleDir, manifest }) => { + expect(() => assertMobileWebBundleBuilt(bundleDir)).not.toThrow() + expect(manifest.entrypoint).toBe('index.html') + expect(manifest.assets.length).toBeGreaterThanOrEqual(3) + expect( + new Set(manifest.assets.map((asset) => asset.contentType)).size + ).toBeGreaterThanOrEqual(2) + }) + }) + + it('fails on a file the manifest does not list, so no stale asset ships inside asar', async () => { + await withBundle(async ({ bundleDir }) => { + // An asset dropped from the manifest keeps its content-addressed name, so nothing ever + // overwrites it; without this check it packs unreachable and unverified. + await writeFile(join(bundleDir, 'assets', 'stale.js'), '// from an earlier build\n', 'utf8') + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow( + /does not list: assets\/stale\.js/ + ) + }) + }) + + it('accepts exactly the manifest, the entrypoint and the listed assets', async () => { + await withBundle(async ({ bundleDir, manifest }) => { + const onDisk = (await readdir(bundleDir, { recursive: true, withFileTypes: true })) + .filter((entry) => entry.isFile()) + .map((entry) => join(entry.parentPath, entry.name).slice(bundleDir.length + 1)) + expect(onDisk.toSorted()).toEqual( + ['manifest.json', ...manifest.assets.map((asset) => asset.path)].toSorted() + ) + }) + }) + + it('fails when the manifest is missing', async () => { + const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-guard-')) + try { + expect(() => assertMobileWebBundleBuilt(scratch)).toThrow(/no bundle manifest/) + } finally { + await rm(scratch, { recursive: true, force: true }) + } + }) + + it('fails when the manifest is not JSON', async () => { + await withBundle(async ({ bundleDir }) => { + await writeFile(join(bundleDir, 'manifest.json'), 'not json', 'utf8') + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/not valid JSON/) + }) + }) + + it('fails when an asset is tampered with on disk', async () => { + await withBundle(async ({ bundleDir, manifest }) => { + const asset = manifest.assets.find((entry) => entry.path.endsWith('.js')) + const bytes = await readFile(join(bundleDir, asset.path)) + // Same length, different content: only the hash check can catch this. + bytes[bytes.length - 1] = bytes.at(-1) === 0x20 ? 0x09 : 0x20 + await writeFile(join(bundleDir, asset.path), bytes) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/hashes to .* on disk/) + }) + }) + + it('fails when an asset is truncated', async () => { + await withBundle(async ({ bundleDir, manifest }) => { + const asset = manifest.assets.find((entry) => entry.path.endsWith('.css')) + await writeFile(join(bundleDir, asset.path), 'truncated', 'utf8') + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/bytes on disk, manifest says/) + }) + }) + + it('fails when a listed asset was never written', async () => { + await withBundle(async ({ bundleDir, manifest }) => { + const asset = manifest.assets.find((entry) => entry.path.endsWith('.png')) + await rm(join(bundleDir, asset.path)) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/which is missing from/) + }) + }) + + it('fails when the manifest buildId no longer matches its asset list', async () => { + await withBundle(async ({ bundleDir }) => { + await rewriteManifest(bundleDir, (manifest) => { + manifest.buildId = 'f'.repeat(64) + }) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/does not match its asset list/) + }) + }) + + it('fails on an unknown schemaVersion', async () => { + await withBundle(async ({ bundleDir }) => { + await rewriteManifest(bundleDir, (manifest) => { + manifest.schemaVersion = 2 + }) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow( + /unsupported manifest schemaVersion/ + ) + }) + }) + + it('fails when a required field is dropped', async () => { + await withBundle(async ({ bundleDir }) => { + await rewriteManifest(bundleDir, (manifest) => { + delete manifest.desktopVersion + }) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/desktopVersion is missing/) + }) + }) + + it('fails when totalBytes disagrees with the asset list', async () => { + await withBundle(async ({ bundleDir }) => { + await rewriteManifest(bundleDir, (manifest) => { + manifest.totalBytes += 1 + }) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/its assets sum to/) + }) + }) + + it('refuses an asset path that escapes the bundle directory', async () => { + await withBundle(async ({ bundleDir }) => { + await rewriteManifest(bundleDir, (manifest) => { + manifest.assets[0].path = '../outside.js' + }) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/not a safe relative path/) + }) + }) + + it('refuses a manifest whose entrypoint is not one of its assets', async () => { + await withBundle(async ({ bundleDir }) => { + await rewriteManifest(bundleDir, (manifest) => { + manifest.entrypoint = 'index.html' + manifest.assets = manifest.assets.filter((asset) => asset.path !== 'index.html') + }) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/is not one of its assets/) + }) + }) +}) + +describe('electron-builder packaging wiring', () => { + it('excludes the mobile-web source tree from app.asar', () => { + expect(electronBuilderConfig.files).toContain('!src/mobile-web{,/**/*}') + // The source tree lives under src/, which is excluded wholesale; the explicit entry above + // only survives as a marker, so assert the broad rule is still what does the work. + expect(electronBuilderConfig.files).toContain('!src{,/**/*}') + }) + + it('does not exclude the built bundle, so out/mobile-web ships like out/web', () => { + const excludesBuiltBundle = electronBuilderConfig.files.some( + (entry) => typeof entry === 'string' && entry.startsWith('!out/mobile-web') + ) + expect(excludesBuiltBundle).toBe(false) + }) + + it('runs the bundle guard in beforePack', () => { + expect(String(electronBuilderConfig.beforePack)).toContain('assertMobileWebBundleBuilt') + }) + + it('defaults the bundle root to out/mobile-web when electron-builder calls it', () => { + expect(MOBILE_WEB_BUNDLE_DIR).toBe(join(REPO_ROOT, 'out', 'mobile-web')) + // electron-builder passes the context alone, so the default is what ships. + expect(electronBuilderConfig.beforePack.length).toBe(1) + }) + + it('verifies the bundle root it is given, not the repo one', async () => { + // The seam exists so unit tests need no built out/; it would be worthless if the root were + // accepted and then ignored. + await withBundle(async ({ bundleDir }) => { + await rm(join(bundleDir, 'manifest.json')) + expect(() => + electronBuilderConfig.beforePack( + { electronPlatformName: process.platform, arch: process.arch === 'arm64' ? 3 : 1 }, + bundleDir + ) + ).toThrow(/no bundle manifest/) + }) + }) +}) diff --git a/config/tsconfig.mobile-web.json b/config/tsconfig.mobile-web.json new file mode 100644 index 00000000000..93436c7a4b3 --- /dev/null +++ b/config/tsconfig.mobile-web.json @@ -0,0 +1,8 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", + "include": ["../src/mobile-web/src/**/*"], + "compilerOptions": { + "composite": true, + "types": [] + } +} 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 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/pending-split-close/README.md b/docs/audits/pending-split-close/README.md new file mode 100644 index 00000000000..e1c4902f166 --- /dev/null +++ b/docs/audits/pending-split-close/README.md @@ -0,0 +1,42 @@ +# Pending split close can omit shell retirement + +A restored split pane can be explicitly closed while its IPC spawn/reattach reply is pending. Its transport has no bound PTY ID yet. The old close path deleted the durable leaf binding and destroyed the unbound transport without requesting retirement. The late-result cleanup deliberately preserved reattach and cold-restore replies for remounts, so an existing shell or newly cold-restored shell could remain live with no pane and no kill request. + +This is a concrete mechanism matching the **missing kill requests** in [#15210](https://github.com/stablyai/orca/issues/15210). The relevant close, late-result exclusion, and daemon cold-restore behavior exists in both `v1.4.184` and `v1.4.198`. The report does not establish that this sequence produced its 51 shells. It also does not establish a retained Electron-main heap slope or independently explain [#19831](https://github.com/stablyai/orca/issues/19831). + +## Ownership and fix + +The explicit split-close hook captures its durable requested PTY before removing the leaf binding. Existing tab-retirement planning resolves the local/direct-SSH execution owner. It requests retirement immediately and retains an explicit-close callback for the late same-ID result. The late callback rechecks current store ownership and the current transport map, including a replacement at the same leaf. It makes a second request only when no owner remains. + +An eager request alone is insufficient: Electron spawn preflight can still be waiting before the adapter/history lock exists. A kill can return `SessionNotFoundError`, then the pending spawn creates a new cold-restored shell. The late-result request closes that window. If spawn already owns the adapter's history lock, the ordinary known-ID shutdown waits behind it; the control case preserves that behavior. + +Generic detach/destroy keeps its existing behavior. Repeated destroy retains explicit intent, while a different returned reattach identity remains protected. The tab aggregate/row ID is not counted as a separate same-tab owner; it can be the closing leaf's own stale index. Other live tabs use all existing retirement ownership sources; remote alias matching reuses the existing normalized identity. + +Paired-runtime handles, runtime-owned native hints, and unresolved owners never fall through to local kill. They are outside this IPC fix. A provider failure is logged by the existing retirement helper; requesting retirement is not confirmation of process death. No host inventory sweep, wire change, global tombstone, or remount-driven shutdown is introduced. A shared owner already present at close keeps its established retirement responsibility; this does not change all pending-fresh/shared-owner races. + +## Close-confirmation review correction + +The original tests called `executeClosePane` after a close decision. A separate review found that the public `handleRequestClosePane` callback skipped the running-work check while the transport was still unbound. Retirement now obtains the pending local/direct-SSH identity from the existing retirement plan and runs the existing confirmation flow first. An unverified pending probe asks for confirmation; Cancel preserves the process. Before a delayed decision or confirmation acts, the captured tab generation, pane, transport, binding, and execution owner must still match. A split that became the final pane cannot turn an old confirmation into a whole-tab close. + +`pending-pane-close-confirmation.test.ts` exercises the public callbacks, including live and unverified work, Cancel, confirmed close, completed attachment, ownership replacement, and direct SSH. These are separate regression controls added after the original comparative proof below; they do not change its historical case counts. + +## Reproduce + +From the repository root with the existing dependencies installed: + +```sh +ORCA_BACKGROUND_LAUNCH=1 node docs/audits/pending-split-close/reproduce.mjs +``` + +The script runs the actual close hook, layout binding, pane-close handler, IPC transport, daemon server, and daemon adapter. React registration and unrelated presentation/status callbacks are mocked. Daemon cases use temporary sockets, synthetic history, and the existing fake subprocess fixture; no real shell or Orca window is launched. It cleans its temporary configuration and invokes Vitest through the repository's cross-platform `runProcess`. + +`fix.patch` is reversed only inside a temporary Vite source transform for the baseline. Working files remain unchanged. New helper/test sources remain present, but the baseline close hook cannot call the helper. Source hashes and exact cases are recorded in `results.json`: + +| Version | Passed | Failed | +| --- | ---: | ---: | +| Before fix | 10 | 14 | +| With fix | 24 | 0 | + +The 24 cases retain the original nine reproduction/control scenarios and add same-leaf/new-map/different-tab adoption, sibling ownership, repeated destroy, provider failure/retry, spawn rejection, returned-ID mismatch, direct SSH, unresolved/paired-runtime routing, and folder workspace coverage. Eight separate ownership-query tests cover legacy/scoped aliases and the existing tab ownership sources. The baseline failures include assertions about the new eager request; they are not 14 independent leaks. + +Historical entry points: `v1.4.184` `TerminalPane.tsx:1163`, `use-terminal-pane-lifecycle.ts:1368`, `pty-transport.ts:859`, and `src/main/daemon/daemon-pty-adapter.ts:750`. The current equivalents are `use-terminal-pane-close-actions.ts`, `terminal-pane-pane-closed.ts`, `ipc-pty-connect.ts`, and `daemon-pty-spawn-result.ts`. diff --git a/docs/audits/pending-split-close/daemon-proof.test.ts b/docs/audits/pending-split-close/daemon-proof.test.ts new file mode 100644 index 00000000000..3a722fab071 --- /dev/null +++ b/docs/audits/pending-split-close/daemon-proof.test.ts @@ -0,0 +1,132 @@ +import { mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { expect, it, vi } from 'vitest' +import { preparePendingSplitClose } from '../../../src/renderer/src/components/terminal-pane/pending-split-close-test-fixture' +import { + startDaemonAdapterHarness, + createMockSubprocess +} from '../../../src/main/daemon/daemon-pty-adapter-test-harness' +import { DaemonPtyAdapter } from '../../../src/main/daemon/daemon-pty-adapter' +import { getHistorySessionDirName } from '../../../src/main/daemon/history-paths' +import { SessionNotFoundError } from '../../../src/main/daemon/types' + +async function coldRestoreHarness() { + const subprocess = createMockSubprocess() + const h = await startDaemonAdapterHarness(() => subprocess) + const historyPath = join(h.dir, 'history') + const sessionDir = join(historyPath, getHistorySessionDirName('pty-restored')) + mkdirSync(sessionDir, { recursive: true }) + writeFileSync( + join(sessionDir, 'meta.json'), + JSON.stringify({ + cwd: h.dir, + cols: 80, + rows: 24, + startedAt: '2026-04-15T10:00:00Z', + endedAt: null, + exitCode: null + }) + ) + writeFileSync(join(sessionDir, 'scrollback.bin'), 'synthetic saved history\r\n') + const adapter = new DaemonPtyAdapter({ + socketPath: h.socketPath, + tokenPath: h.tokenPath, + historyPath + }) + const requests: Promise[] = [] + const absent: string[] = [] + const bridgeKill = (id: string): Promise => { + const request = adapter.shutdown(id, { immediate: true }).catch((error) => { + // The renderer IPC handler treats the provider's already-gone reply as success. + if (error instanceof SessionNotFoundError) { + absent.push(id) + return + } + throw error + }) + requests.push(request) + return request + } + return { + adapter, + subprocess, + requests, + absent, + bridgeKill, + async dispose() { + adapter.dispose() + h.adapter.dispose() + await h.server.shutdown() + rmSync(h.dir, { recursive: true, force: true }) + } + } +} + +it('explicit split close retires a real daemon cold restore whose reply is pending', async () => { + const h = await coldRestoreHarness() + try { + const p = await preparePendingSplitClose() + vi.mocked(window.api.pty.kill).mockImplementation(h.bridgeKill) + const result = await h.adapter.spawn({ cols: 80, rows: 24, sessionId: 'pty-restored' }) + expect(result.isReattach).not.toBe(true) + expect(result.coldRestore).toBeDefined() + expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(true) + p.actions.executeClosePane(1) + p.spawn.resolve(result) + await p.connecting + await Promise.all(h.requests) + expect(window.api.pty.kill).toHaveBeenCalledTimes(2) + expect(h.subprocess.forceKill).toHaveBeenCalledOnce() + expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(false) + } finally { + await h.dispose() + } +}) + +it('the explicit late reply retries a kill that completed before adapter admission', async () => { + const h = await coldRestoreHarness() + try { + const p = await preparePendingSplitClose() + vi.mocked(window.api.pty.kill).mockImplementation(h.bridgeKill) + p.actions.executeClosePane(1) + // Hold admission outside the adapter; there is no history lock or session yet. + await Promise.all(h.requests) + expect(h.absent).toEqual(['pty-restored']) + const result = await h.adapter.spawn({ cols: 80, rows: 24, sessionId: 'pty-restored' }) + expect(result.coldRestore).toBeDefined() + expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(true) + p.spawn.resolve(result) + await p.connecting + await Promise.all(h.requests) + expect(window.api.pty.kill).toHaveBeenCalledTimes(2) + expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(false) + } finally { + await h.dispose() + } +}) + +it('a known-ID shutdown already admitted to the adapter waits for its spawn history lock', async () => { + const h = await coldRestoreHarness() + const started = Promise.withResolvers() + const release = Promise.withResolvers() + const finish = h.adapter['finishSpawn'].bind(h.adapter) + h.adapter['finishSpawn'] = async (context, result) => { + started.resolve() + await release.promise + return finish(context, result) + } + try { + const spawning = h.adapter.spawn({ cols: 80, rows: 24, sessionId: 'pty-restored' }) + await started.promise + const stopping = h.adapter.shutdown('pty-restored', { immediate: true }) + expect(h.subprocess.forceKill).not.toHaveBeenCalled() + release.resolve() + await spawning + await stopping + expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(false) + expect(h.subprocess.forceKill).toHaveBeenCalledOnce() + } finally { + release.resolve() + await h.dispose() + } +}) diff --git a/docs/audits/pending-split-close/fix.patch b/docs/audits/pending-split-close/fix.patch new file mode 100644 index 00000000000..65a7ad808b8 --- /dev/null +++ b/docs/audits/pending-split-close/fix.patch @@ -0,0 +1,237 @@ +diff --git a/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts b/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts +index 3023236de0e..52b983c51fd 100644 +--- a/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts ++++ b/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts +@@ -27,6 +27,7 @@ type IpcPtyConnectContext = { + /** True only for the one buffered exit consumed by this connect attempt. */ + isExpectedExitCurrent: () => boolean + ownsPtyId: (id: string) => boolean ++ handleExplicitlyClosedConnect?: (id: string) => boolean + bind: (id: string) => void + isCurrent: (id: string) => boolean + setCallbacks: (callbacks: PtyConnectOptions['callbacks']) => void +@@ -89,6 +90,9 @@ export async function connectIpcPty( + const priorIncarnationFence = currentPreHandlerPtySequence() + const spawnResult = await spawnIpcPty(transportOptions, options, admittedSessionId) + const retireFreshSpawn = async (): Promise => { ++ if (context.handleExplicitlyClosedConnect?.(spawnResult.id)) { ++ return ++ } + // A newer generation may already own a recycled id; an id-only kill would retire its PTY. + if ( + !spawnResult.isReattach && +diff --git a/src/renderer/src/components/terminal-pane/pty-transport-types.ts b/src/renderer/src/components/terminal-pane/pty-transport-types.ts +index 4d7eaf7358b..2f6ed734bbf 100644 +--- a/src/renderer/src/components/terminal-pane/pty-transport-types.ts ++++ b/src/renderer/src/components/terminal-pane/pty-transport-types.ts +@@ -232,7 +232,10 @@ export type PtyTransport = { + * it also drops the transport's output processor from the pty side-effect memory census, + * so a reattached one would run untracked. Create a new transport instead. */ + detach?: (options?: { preserveExitObserver?: boolean }) => void +- destroy?: () => void | Promise ++ destroy?: (options?: { ++ /** Explicit close can retain retirement intent until an unbound connect settles. */ ++ onAbandonedConnect?: (ptyId: string) => boolean ++ }) => void | Promise + } + + export type IpcPtyTransportOptions = { +diff --git a/src/renderer/src/components/terminal-pane/pty-transport.ts b/src/renderer/src/components/terminal-pane/pty-transport.ts +index f794b9a1e4d..7731e75eac5 100644 +--- a/src/renderer/src/components/terminal-pane/pty-transport.ts ++++ b/src/renderer/src/components/terminal-pane/pty-transport.ts +@@ -44,6 +44,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra + } = opts + let connected = false + let destroyed = false ++ let onAbandonedConnect: ((ptyId: string) => boolean) | undefined + let ptyId: string | null = null + let lifecycleGeneration = 0 + let lastExitGeneration: number | null = null +@@ -137,6 +138,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra + lastExitGeneration === lifecycleGeneration && + lifecycleGeneration === connectGeneration + 1, + ownsPtyId: (id) => !destroyed && connected && ptyId === id, ++ handleExplicitlyClosedConnect: (id) => destroyed && (onAbandonedConnect?.(id) ?? false), + bind, + isCurrent: (id) => lifecycleGeneration === connectGeneration && connected && ptyId === id, + setCallbacks, +@@ -268,7 +270,8 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra + : { ...(opts.cwd ? { cwd: opts.cwd } : {}), ...(shellOverride ? { shellOverride } : {}) }, + resetCrossChunkParserState: outputProcessor.resetAgentStatusCarry, + +- destroy() { ++ destroy(options) { ++ onAbandonedConnect ??= options?.onAbandonedConnect + destroyed = true + try { + this.disconnect() +diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts +index 330b42166cd..ea85e929e81 100644 +--- a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts ++++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts +@@ -1,4 +1,4 @@ +-import { useCallback, useImperativeHandle } from 'react' ++import { useCallback, useImperativeHandle, useRef } from 'react' + import { useAppStore } from '../../store' + import type { PaneExternalDropTarget } from '@/lib/pane-manager/pane-manager' + import { makePaneKey } from '../../../../shared/stable-pane-id' +@@ -13,8 +13,11 @@ import { + } from './terminal-pane-tab-detach' + import { clearPaneTerminalError } from './terminal-error-accumulation' + import type { TerminalPaneBindingController } from './use-terminal-pane-layout-bindings' ++import { retireUnboundIpcTerminalPane } from './retire-unbound-ipc-terminal-pane' ++import { capturePendingTerminalPaneClose } from './terminal-pane-close-admission' + + export function useTerminalPaneCloseActions(controller: TerminalPaneBindingController) { ++ const confirmedCloseRef = useRef<(() => void) | null>(null) + const { + clearSessionRestoredBannerForPane, + managerRef, +@@ -46,6 +49,13 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr + clearSessionRestoredBannerForPane(paneId) + const leafId = manager.getLeafId(paneId) + if (leafId) { ++ retireUnboundIpcTerminalPane({ ++ getState: useAppStore.getState, ++ tabId, ++ leafId, ++ transport: paneTransportsRef.current.get(paneId), ++ getTransports: () => paneTransportsRef.current ++ }) + useAppStore.getState().setCacheTimerStartedAt(makePaneKey(tabId, leafId), null) + useAppStore.getState().dropAgentStatus(makePaneKey(tabId, leafId), { paneRemoved: true }) + } +@@ -79,12 +89,18 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr + return + } + const transport = paneTransportsRef.current.get(paneId) +- const ptyId = transport?.getPtyId() ++ const pending = capturePendingTerminalPaneClose(controller, paneId, useAppStore.getState) ++ const ptyId = transport?.getPtyId() ?? pending?.ptyId + if (!ptyId) { + executeClosePane(paneId) + return + } + const settings = useAppStore.getState().settings ++ const close = (): void => { ++ if (!pending || pending.isCurrent()) { ++ executeClosePane(paneId) ++ } ++ } + let decided = false + const decide = (act: () => void): void => { + if (decided) { +@@ -93,12 +109,23 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr + decided = true + act() + } +- const confirmClose = (): void => ++ const confirmClose = (): void => { ++ if (pending && !pending.isCurrent()) { ++ return ++ } ++ confirmedCloseRef.current = close + setPendingCloseConfirmation({ + paneId, + copyKind: getCloseDialogCopyKind(paneId) + }) +- const probeTimeout = setTimeout(() => decide(confirmClose), RUNNING_CLOSE_PROBE_TIMEOUT_MS) ++ } ++ const probeTimeout = setTimeout( ++ () => ++ decide( ++ pending && settings?.skipCloseTerminalWithRunningProcessConfirm ? close : confirmClose ++ ), ++ RUNNING_CLOSE_PROBE_TIMEOUT_MS ++ ) + // Why the shared probe rather than a direct inspect: this is the same question the tab-close + // guard asks, and the two must not drift on what an unanswered host means. + void probePtyRunningWork(settings, [ptyId], { timeoutMs: RUNNING_CLOSE_PROBE_TIMEOUT_MS }) +@@ -106,10 +133,10 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr + clearTimeout(probeTimeout) + decide(() => { + if ( +- probes[0]?.verdict !== 'live' || ++ (pending ? probes[0]?.verdict === 'exited' : probes[0]?.verdict !== 'live') || + settings?.skipCloseTerminalWithRunningProcessConfirm + ) { +- executeClosePane(paneId) ++ close() + } else { + confirmClose() + } +@@ -117,7 +144,9 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr + }) + .catch(() => { + clearTimeout(probeTimeout) +- decide(() => executeClosePane(paneId)) ++ decide( ++ pending && !settings?.skipCloseTerminalWithRunningProcessConfirm ? confirmClose : close ++ ) + }) + }, + // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. +@@ -143,22 +172,24 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr + }, []) + const handleConfirmClose = useCallback( + (dontAskAgain: boolean) => { +- if (pendingCloseConfirmation === null) { ++ if (pendingCloseConfirmation === null || confirmedCloseRef.current === null) { + return + } +- const paneId = pendingCloseConfirmation.paneId ++ const confirmedClose = confirmedCloseRef.current ++ confirmedCloseRef.current = null + setPendingCloseConfirmation(null) + if (dontAskAgain) { + void updateSettings({ + skipCloseTerminalWithRunningProcessConfirm: true + }) + } +- executeClosePane(paneId) ++ confirmedClose() + }, + // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. + [executeClosePane, pendingCloseConfirmation, updateSettings] + ) + const handleCancelClose = useCallback(() => { ++ confirmedCloseRef.current = null + setPendingCloseConfirmation(null) + // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. + }, []) +diff --git a/src/renderer/src/store/slices/terminal-tab-retirement.ts b/src/renderer/src/store/slices/terminal-tab-retirement.ts +index 80e67824699..e2e034fa6af 100644 +--- a/src/renderer/src/store/slices/terminal-tab-retirement.ts ++++ b/src/renderer/src/store/slices/terminal-tab-retirement.ts +@@ -135,6 +135,30 @@ export function isTerminalTabPresent( + return locateTerminalTab(state.tabsByWorktree, tabId) !== null + } + ++export function hasTerminalPtyOwnerOutsidePane( ++ state: TerminalTabRetirementState, ++ identity: string, ++ tabId: string, ++ excludedLeafId?: string ++): boolean { ++ for (const [ownerTabId, owner] of collectLiveTerminalTabs(state)) { ++ const ids = ++ ownerTabId === tabId ++ ? Object.entries(state.terminalLayoutsByTabId[tabId]?.ptyIdsByLeafId ?? {}) ++ .filter(([leafId]) => leafId !== excludedLeafId) ++ .map(([, ptyId]) => ptyId) ++ : collectPtyIdsForTab(state, ownerTabId, owner.rowPtyId) ++ if ( ++ ids.some( ++ (ptyId) => getTerminalPtyOwnershipIdentity(state, ptyId, owner.worktreeId) === identity ++ ) ++ ) { ++ return true ++ } ++ } ++ return false ++} ++ + export function buildTerminalTabRetirementPlan( + state: TerminalTabRetirementState, + tabId: string diff --git a/docs/audits/pending-split-close/reproduce.mjs b/docs/audits/pending-split-close/reproduce.mjs new file mode 100644 index 00000000000..73b33c09e30 --- /dev/null +++ b/docs/audits/pending-split-close/reproduce.mjs @@ -0,0 +1,152 @@ +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') + } +} + +for (const path of [ + 'src/renderer/src/components/terminal-pane/retire-unbound-ipc-terminal-pane.ts', + 'src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts', + 'src/renderer/src/components/terminal-pane/pending-split-close.test.ts', + 'docs/audits/pending-split-close/daemon-proof.test.ts' +]) { + sourceHashes[path] = { + current: createHash('sha256') + .update(await readFile(resolve(root, path))) + .digest('hex') + } +} + +const scratch = await mkdtemp(join(tmpdir(), 'orca-pending-split-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 = [ + 'src/renderer/src/components/terminal-pane/pending-split-close.test.ts', + 'docs/audits/pending-split-close/daemon-proof.test.ts' + ] + 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: 'pending-split-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 === 14 && + before.passed === 10 && + before.passed + before.failed === 24 && + after.passed === 24 && + after.failed === 0 + console.log( + JSON.stringify( + { + comparison: + 'Actual split close/IPC transport and temporary daemon socket 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/pending-split-close/results.json b/docs/audits/pending-split-close/results.json new file mode 100644 index 00000000000..7e694e953b2 --- /dev/null +++ b/docs/audits/pending-split-close/results.json @@ -0,0 +1,65 @@ +{ + "comparison": "Actual split close/IPC transport and temporary daemon socket tests; before reverses only fix.patch in a temporary Vite transform", + "sourceHashes": { + "src/renderer/src/components/terminal-pane/ipc-pty-connect.ts": { + "before": "7fe38b17d9a1c105bd93088d70086feb0c08f7f80f4513ffd975c5b4bc2167b0", + "after": "29985c0b538f701341fb8eca383386ad20f9326090dff452d4b2703b49fc4ea3" + }, + "src/renderer/src/components/terminal-pane/pty-transport-types.ts": { + "before": "ab404437789ca3f7da2e8d0757778a1098bbe43dc4e04c5b0f112e7a4456c11c", + "after": "15dcd4cb02cceffca4c3b880f6b2688fbd14e6ea1421984d5873f100fa39b5fb" + }, + "src/renderer/src/components/terminal-pane/pty-transport.ts": { + "before": "0455dba126457eb50a6075a8b265b29016aa5ed8c0730948e23d4f864bd8d5b3", + "after": "b5ac687bbb5c21e94a433d4a66f9b8fca46b3004f331f128c44bfa21b32a3838" + }, + "src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts": { + "before": "978003ccd82d0af5dce5501edaa7c20c87170e590a5f3f06e55b28f9e4f0323f", + "after": "bbfeb2385fd120a7a1407d043011fb689b5c2b652c31150de44061f1edb76a7b" + }, + "src/renderer/src/store/slices/terminal-tab-retirement.ts": { + "before": "e622afc63524826732f1779a6180d17b51ac5a2c705654062cc0907ea735db4f", + "after": "21a5f350885851398e316201686e073e3560c107b4b9d1b2e08eea6e07ec980f" + }, + "src/renderer/src/components/terminal-pane/retire-unbound-ipc-terminal-pane.ts": { + "current": "3194229bdd3c992e8459cdad727a3931653953b204854486b8aaf4d129152d24" + }, + "src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts": { + "current": "5fddc75178546161448f8a84b43f0e7da41c7dbb986682a09f8f7ec4e44617bf" + }, + "src/renderer/src/components/terminal-pane/pending-split-close.test.ts": { + "current": "106abb33917dd403606af413558bfe95730a064199d4f7085c4970d38cfde0cc" + }, + "docs/audits/pending-split-close/daemon-proof.test.ts": { + "current": "0a29132942a9f8aa60ceada3498fb4fb344c1c0e14a8c9b4cbc8ed4cfa9543d9" + } + }, + "before": { + "exitCode": 1, + "passed": 10, + "failed": 14, + "failedCases": [ + "explicit split close retires a real daemon cold restore whose reply is pending", + "the explicit late reply retries a kill that completed before adapter admission", + "explicit split close retires a pending reattach before and after its reply", + "explicit split close retires a pending cold-restore-new before and after its reply", + "explicit split close retires a pending ordinary-fresh before and after its reply", + "protects a same-leaf replacement that adopts after explicit close", + "protects a different-tab replacement that adopts after explicit close", + "protects a new-transport-map replacement that adopts after explicit close", + "retains explicit close intent across repeated generic destroy calls", + "preserves a different returned reattach identity", + "retries an eager provider failure when the same-ID reply arrives", + "does not invent a late session after a rejected spawn", + "routes direct SSH retirement through the existing IPC identity", + "retires a local folder-workspace split without a git worktree row" + ] + }, + "after": { + "exitCode": 0, + "passed": 24, + "failed": 0, + "failedCases": [] + }, + "passed": true +} 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/mobile/README.md b/mobile/README.md index e78c2c410bf..be17a867702 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -135,11 +135,24 @@ Run these checks before committing mobile terminal changes: ```bash cd mobile pnpm exec tsc --noEmit +pnpm run check:tests-typecheck pnpm lint cd .. pnpm typecheck:node ``` +`tsc --noEmit` reads `tsconfig.json`, which excludes test files so Metro never bundles them. +`tsconfig.test.json` puts them back, and `pnpm run typecheck:tests` shows their errors in full. +`check:tests-typecheck` is the gate over it: a ratchet against `tests-typecheck-baseline.txt`, the +127 test files that do not typecheck yet. It fails when a file that checks today stops checking, +and when a baseline entry starts checking (prune it with +`node scripts/check-tests-typecheck-ratchet.mjs --prune`). The list may only shrink. + +The same gate censuses the program first: every `*.test.ts(x)` on disk must be in it, or named in +the script's `TESTS_OUTSIDE_PROGRAM` with a reason. Without that, a test excluded from +`tsconfig.test.json` — or a `Foo.test.tsx` shadowed by a `Foo.test.ts` beside it, which a wildcard +`include` drops for the higher-priority extension — would leave the ratchet silently. + ## Protocol Version Compatibility Mobile and desktop talk over a versioned protocol. Because mobile updates lag desktop by 24-48h via the App Store, both sides exchange version numbers on `status.get` so a genuinely incompatible combo can hard-block instead of silently misbehaving. diff --git a/mobile/app.json b/mobile/app.json index cfe66109e22..5ff4b7a51fe 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Orca", "slug": "orca-mobile", - "version": "0.0.50", + "version": "0.0.51", "orientation": "default", "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", @@ -75,7 +75,7 @@ "allowBackup": false, "permissions": ["RECORD_AUDIO", "MODIFY_AUDIO_SETTINGS"], "package": "com.stably.orca.mobile", - "versionCode": 16, + "versionCode": 18, "googleServicesFile": "./google-services.json" }, "plugins": [ diff --git a/mobile/app/troubleshoot.tsx b/mobile/app/troubleshoot.tsx index d07368b21f9..d64b238b577 100644 --- a/mobile/app/troubleshoot.tsx +++ b/mobile/app/troubleshoot.tsx @@ -1,7 +1,15 @@ import { useRouter } from 'expo-router' +import { MobileWebBundleProbeRow } from '../src/diagnostics/mobile-web-bundle-probe-row' import { TroubleshootView } from '../src/diagnostics/troubleshoot-view' import { useTroubleshootDiagnostics } from '../src/diagnostics/use-troubleshoot-diagnostics' +// Same guard as push-token.ts: `__DEV__` is undefined outside the React Native runtime. The import +// above is static, so a release bundle still carries the row's graph and evaluates its hoisted +// schemas at load; nothing mounts, no host is looked up and no request is made. This repo has no +// `__DEV__`-conditional `require` idiom to trim it with — every `require` in `mobile/src` is a Metro +// asset path — so introducing one is a change for the shell in Phase B, not for this row. +const isDevelopmentBuild = typeof __DEV__ !== 'undefined' && __DEV__ + export default function NativeTroubleshootRoute() { const router = useRouter() const { rootRef, diagnosticStatus, checks, runDiagnostics } = useTroubleshootDiagnostics() @@ -13,6 +21,7 @@ export default function NativeTroubleshootRoute() { runDiagnostics={() => void runDiagnostics()} onBack={() => router.back()} onConnectionLog={() => router.push('/connection-log')} + developerRow={isDevelopmentBuild ? : null} /> ) } diff --git a/mobile/package.json b/mobile/package.json index d86c0d524ef..c4a977cd459 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -10,6 +10,8 @@ "postinstall": "node scripts/build-terminal-webview-engine.mjs && node scripts/build-mermaid-webview-engine.mjs", "test": "vitest run", "typecheck": "tsc --noEmit", + "typecheck:tests": "tsc --noEmit -p tsconfig.test.json", + "check:tests-typecheck": "node scripts/check-tests-typecheck-ratchet.mjs", "lint": "oxlint", "format": "oxfmt --write .", "format:check": "oxfmt --check .", diff --git a/mobile/rpc-foundation/goldens/aivault-history-scan-fulfilled.json b/mobile/rpc-foundation/goldens/aivault-history-scan-fulfilled.json index a1f0b150d42..9c087a374be 100644 --- a/mobile/rpc-foundation/goldens/aivault-history-scan-fulfilled.json +++ b/mobile/rpc-foundation/goldens/aivault-history-scan-fulfilled.json @@ -3,7 +3,7 @@ "family": "aiVault.history", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7add46922ba5486e56c8acd99a53d083b605f5d98f3cc44ee3cb350ec0406080", diff --git a/mobile/rpc-foundation/goldens/aivault-history-scan-unsupported.json b/mobile/rpc-foundation/goldens/aivault-history-scan-unsupported.json index 1a8fe04ab3b..638829922ee 100644 --- a/mobile/rpc-foundation/goldens/aivault-history-scan-unsupported.json +++ b/mobile/rpc-foundation/goldens/aivault-history-scan-unsupported.json @@ -3,7 +3,7 @@ "family": "aiVault.history", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7add46922ba5486e56c8acd99a53d083b605f5d98f3cc44ee3cb350ec0406080", diff --git a/mobile/rpc-foundation/goldens/aivault-history-scan-worktrees-late.json b/mobile/rpc-foundation/goldens/aivault-history-scan-worktrees-late.json index 8fc3d162666..9978849b8a6 100644 --- a/mobile/rpc-foundation/goldens/aivault-history-scan-worktrees-late.json +++ b/mobile/rpc-foundation/goldens/aivault-history-scan-worktrees-late.json @@ -3,7 +3,7 @@ "family": "aiVault.history", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7add46922ba5486e56c8acd99a53d083b605f5d98f3cc44ee3cb350ec0406080", diff --git a/mobile/rpc-foundation/goldens/aivault-history-screen-listed.json b/mobile/rpc-foundation/goldens/aivault-history-screen-listed.json index bec79adb1a3..eab9430cd59 100644 --- a/mobile/rpc-foundation/goldens/aivault-history-screen-listed.json +++ b/mobile/rpc-foundation/goldens/aivault-history-screen-listed.json @@ -3,7 +3,7 @@ "family": "aiVault.history-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3", diff --git a/mobile/rpc-foundation/goldens/aivault-history-screen-worktrees.json b/mobile/rpc-foundation/goldens/aivault-history-screen-worktrees.json index e69e5ec0b49..3ad575d4db6 100644 --- a/mobile/rpc-foundation/goldens/aivault-history-screen-worktrees.json +++ b/mobile/rpc-foundation/goldens/aivault-history-screen-worktrees.json @@ -3,7 +3,7 @@ "family": "aiVault.history-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3", diff --git a/mobile/rpc-foundation/goldens/aivault-resume-launch-create-refused.json b/mobile/rpc-foundation/goldens/aivault-resume-launch-create-refused.json index 49c05a1008d..e1486fd4f31 100644 --- a/mobile/rpc-foundation/goldens/aivault-resume-launch-create-refused.json +++ b/mobile/rpc-foundation/goldens/aivault-resume-launch-create-refused.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/aivault-resume-launch-invalid-tab.json b/mobile/rpc-foundation/goldens/aivault-resume-launch-invalid-tab.json index 10d9f91ea24..38bc92f8e6b 100644 --- a/mobile/rpc-foundation/goldens/aivault-resume-launch-invalid-tab.json +++ b/mobile/rpc-foundation/goldens/aivault-resume-launch-invalid-tab.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/aivault-resume-launch-locked.json b/mobile/rpc-foundation/goldens/aivault-resume-launch-locked.json index 1e526c77a59..1ccb03de2fa 100644 --- a/mobile/rpc-foundation/goldens/aivault-resume-launch-locked.json +++ b/mobile/rpc-foundation/goldens/aivault-resume-launch-locked.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/aivault-resume-launch-sent.json b/mobile/rpc-foundation/goldens/aivault-resume-launch-sent.json index a14ea6cf1ca..8d6b4dc5bf3 100644 --- a/mobile/rpc-foundation/goldens/aivault-resume-launch-sent.json +++ b/mobile/rpc-foundation/goldens/aivault-resume-launch-sent.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/aivault-resume-prepare-refused.json b/mobile/rpc-foundation/goldens/aivault-resume-prepare-refused.json index 994c6e6bbd8..a66ddfd8b42 100644 --- a/mobile/rpc-foundation/goldens/aivault-resume-prepare-refused.json +++ b/mobile/rpc-foundation/goldens/aivault-resume-prepare-refused.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/aivault-resume-prepare-repin.json b/mobile/rpc-foundation/goldens/aivault-resume-prepare-repin.json index 93252c68b03..28508997823 100644 --- a/mobile/rpc-foundation/goldens/aivault-resume-prepare-repin.json +++ b/mobile/rpc-foundation/goldens/aivault-resume-prepare-repin.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/aivault-resume-prepare-skipped.json b/mobile/rpc-foundation/goldens/aivault-resume-prepare-skipped.json index 73cd43371a7..ce575e1c76a 100644 --- a/mobile/rpc-foundation/goldens/aivault-resume-prepare-skipped.json +++ b/mobile/rpc-foundation/goldens/aivault-resume-prepare-skipped.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/aivault-resume-prepare-unavailable.json b/mobile/rpc-foundation/goldens/aivault-resume-prepare-unavailable.json index f20d528761f..cfdbc612399 100644 --- a/mobile/rpc-foundation/goldens/aivault-resume-prepare-unavailable.json +++ b/mobile/rpc-foundation/goldens/aivault-resume-prepare-unavailable.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/b1.json b/mobile/rpc-foundation/goldens/b1.json index 1bfd4d16dec..8c137e1148b 100644 --- a/mobile/rpc-foundation/goldens/b1.json +++ b/mobile/rpc-foundation/goldens/b1.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/b2.json b/mobile/rpc-foundation/goldens/b2.json index c4106dbb1a3..cee86516d62 100644 --- a/mobile/rpc-foundation/goldens/b2.json +++ b/mobile/rpc-foundation/goldens/b2.json @@ -3,7 +3,7 @@ "family": "project-explicit-false", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/b3.json b/mobile/rpc-foundation/goldens/b3.json index f0b0c4bec9e..0846bf836a6 100644 --- a/mobile/rpc-foundation/goldens/b3.json +++ b/mobile/rpc-foundation/goldens/b3.json @@ -3,7 +3,7 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/browser-dialog-accepted.json b/mobile/rpc-foundation/goldens/browser-dialog-accepted.json index 5a92cf0a421..2e32118b188 100644 --- a/mobile/rpc-foundation/goldens/browser-dialog-accepted.json +++ b/mobile/rpc-foundation/goldens/browser-dialog-accepted.json @@ -3,7 +3,7 @@ "family": "browser.dialog", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/browser-dialog-dismissed.json b/mobile/rpc-foundation/goldens/browser-dialog-dismissed.json index 049b365ef67..ef6b8c81d0f 100644 --- a/mobile/rpc-foundation/goldens/browser-dialog-dismissed.json +++ b/mobile/rpc-foundation/goldens/browser-dialog-dismissed.json @@ -3,7 +3,7 @@ "family": "browser.dialog", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/browser-keyboard-input.json b/mobile/rpc-foundation/goldens/browser-keyboard-input.json index 2c87410c7ec..860c57da1af 100644 --- a/mobile/rpc-foundation/goldens/browser-keyboard-input.json +++ b/mobile/rpc-foundation/goldens/browser-keyboard-input.json @@ -3,7 +3,7 @@ "family": "browser.keyboard", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/browser-pointer-click-accepted.json b/mobile/rpc-foundation/goldens/browser-pointer-click-accepted.json index c42fc9d81ae..10fd6455b47 100644 --- a/mobile/rpc-foundation/goldens/browser-pointer-click-accepted.json +++ b/mobile/rpc-foundation/goldens/browser-pointer-click-accepted.json @@ -3,7 +3,7 @@ "family": "browser.pointer-click", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/browser-pointer-click-fallback.json b/mobile/rpc-foundation/goldens/browser-pointer-click-fallback.json index 2811788fb67..13a8575e2d2 100644 --- a/mobile/rpc-foundation/goldens/browser-pointer-click-fallback.json +++ b/mobile/rpc-foundation/goldens/browser-pointer-click-fallback.json @@ -3,7 +3,7 @@ "family": "browser.pointer-click", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/browser-wheel-scrolled.json b/mobile/rpc-foundation/goldens/browser-wheel-scrolled.json index ba4eaae2f0d..7f03d11b14c 100644 --- a/mobile/rpc-foundation/goldens/browser-wheel-scrolled.json +++ b/mobile/rpc-foundation/goldens/browser-wheel-scrolled.json @@ -3,7 +3,7 @@ "family": "browser.wheel", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-attachment-anonymous.json b/mobile/rpc-foundation/goldens/clipboard-image-attachment-anonymous.json index 18fe9732bdd..40499472cf9 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-attachment-anonymous.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-attachment-anonymous.json @@ -3,7 +3,7 @@ "family": "clipboard.image-attachment", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-attachment-blocked-before-send.json b/mobile/rpc-foundation/goldens/clipboard-image-attachment-blocked-before-send.json index 35294e65642..6c87b9c24fc 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-attachment-blocked-before-send.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-attachment-blocked-before-send.json @@ -3,7 +3,7 @@ "family": "clipboard.image-attachment", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-attachment-cancelled.json b/mobile/rpc-foundation/goldens/clipboard-image-attachment-cancelled.json index 49dfe6bd8b9..1645ab66eee 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-attachment-cancelled.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-attachment-cancelled.json @@ -3,7 +3,7 @@ "family": "clipboard.image-attachment", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-attachment-pasted.json b/mobile/rpc-foundation/goldens/clipboard-image-attachment-pasted.json index 98215f436b2..9e9ef92db17 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-attachment-pasted.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-attachment-pasted.json @@ -3,7 +3,7 @@ "family": "clipboard.image-attachment", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-attachment-upload-refused.json b/mobile/rpc-foundation/goldens/clipboard-image-attachment-upload-refused.json index a3bdb5c3b42..52e53f73ee7 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-attachment-upload-refused.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-attachment-upload-refused.json @@ -3,7 +3,7 @@ "family": "clipboard.image-attachment", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-upload-aborts-on-chunk-failure.json b/mobile/rpc-foundation/goldens/clipboard-image-upload-aborts-on-chunk-failure.json index 7638c537ac0..4406c87bbba 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-upload-aborts-on-chunk-failure.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-upload-aborts-on-chunk-failure.json @@ -3,7 +3,7 @@ "family": "clipboard.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-upload-chunked.json b/mobile/rpc-foundation/goldens/clipboard-image-upload-chunked.json index 6adda9bd1e3..97803218f94 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-upload-chunked.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-upload-chunked.json @@ -3,7 +3,7 @@ "family": "clipboard.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-upload-single-frame-fallback.json b/mobile/rpc-foundation/goldens/clipboard-image-upload-single-frame-fallback.json index 950ebfd54f2..d70f943b27f 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-upload-single-frame-fallback.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-upload-single-frame-fallback.json @@ -3,7 +3,7 @@ "family": "clipboard.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/clipboard-image-upload-start-refused.json b/mobile/rpc-foundation/goldens/clipboard-image-upload-start-refused.json index 7706b705cfc..7d54ad13020 100644 --- a/mobile/rpc-foundation/goldens/clipboard-image-upload-start-refused.json +++ b/mobile/rpc-foundation/goldens/clipboard-image-upload-start-refused.json @@ -3,7 +3,7 @@ "family": "clipboard.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/codex-reset-credit-consumed.json b/mobile/rpc-foundation/goldens/codex-reset-credit-consumed.json index c12807026d8..95683cb0275 100644 --- a/mobile/rpc-foundation/goldens/codex-reset-credit-consumed.json +++ b/mobile/rpc-foundation/goldens/codex-reset-credit-consumed.json @@ -3,7 +3,7 @@ "family": "components.codex-reset-credit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "76b53dea504a688493843e23e8e7d052fc196f559bb1e2579d0fd38bf3156d0e", diff --git a/mobile/rpc-foundation/goldens/codex-reset-credit-resumed.json b/mobile/rpc-foundation/goldens/codex-reset-credit-resumed.json index e0342bc2b5a..ade586d43d5 100644 --- a/mobile/rpc-foundation/goldens/codex-reset-credit-resumed.json +++ b/mobile/rpc-foundation/goldens/codex-reset-credit-resumed.json @@ -3,7 +3,7 @@ "family": "components.codex-reset-credit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "76b53dea504a688493843e23e8e7d052fc196f559bb1e2579d0fd38bf3156d0e", diff --git a/mobile/rpc-foundation/goldens/components-codex-capability.json b/mobile/rpc-foundation/goldens/components-codex-capability.json index 02b2f79ccf5..9fcba28c07e 100644 --- a/mobile/rpc-foundation/goldens/components-codex-capability.json +++ b/mobile/rpc-foundation/goldens/components-codex-capability.json @@ -3,7 +3,7 @@ "family": "components.codex-reset-capability", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/components-setup-ask.json b/mobile/rpc-foundation/goldens/components-setup-ask.json index 1fda7000207..448bba08a94 100644 --- a/mobile/rpc-foundation/goldens/components-setup-ask.json +++ b/mobile/rpc-foundation/goldens/components-setup-ask.json @@ -3,7 +3,7 @@ "family": "components.setup-script", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/components-target-local.json b/mobile/rpc-foundation/goldens/components-target-local.json index 8c102276b32..584dff58131 100644 --- a/mobile/rpc-foundation/goldens/components-target-local.json +++ b/mobile/rpc-foundation/goldens/components-target-local.json @@ -3,7 +3,7 @@ "family": "components.execution-target-local", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/components-target-ssh.json b/mobile/rpc-foundation/goldens/components-target-ssh.json index 3e5fbc34416..3041922356f 100644 --- a/mobile/rpc-foundation/goldens/components-target-ssh.json +++ b/mobile/rpc-foundation/goldens/components-target-ssh.json @@ -3,7 +3,7 @@ "family": "components.execution-target", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/diff-review-branch-compare.json b/mobile/rpc-foundation/goldens/diff-review-branch-compare.json index 1d97c9a77a4..62103ec119d 100644 --- a/mobile/rpc-foundation/goldens/diff-review-branch-compare.json +++ b/mobile/rpc-foundation/goldens/diff-review-branch-compare.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/diff-review-branch-file-diff.json b/mobile/rpc-foundation/goldens/diff-review-branch-file-diff.json index bdbc3f75bf3..82aca40040a 100644 --- a/mobile/rpc-foundation/goldens/diff-review-branch-file-diff.json +++ b/mobile/rpc-foundation/goldens/diff-review-branch-file-diff.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/diff-review-notes-refused-before-compare.json b/mobile/rpc-foundation/goldens/diff-review-notes-refused-before-compare.json index e4c2d2d8313..52f956516b4 100644 --- a/mobile/rpc-foundation/goldens/diff-review-notes-refused-before-compare.json +++ b/mobile/rpc-foundation/goldens/diff-review-notes-refused-before-compare.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/diff-review-refused-file-diff.json b/mobile/rpc-foundation/goldens/diff-review-refused-file-diff.json index 9a638007a34..6cf14bfa6cf 100644 --- a/mobile/rpc-foundation/goldens/diff-review-refused-file-diff.json +++ b/mobile/rpc-foundation/goldens/diff-review-refused-file-diff.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/diff-review-snapshot.json b/mobile/rpc-foundation/goldens/diff-review-snapshot.json index 05da9203d3a..2396ef2c684 100644 --- a/mobile/rpc-foundation/goldens/diff-review-snapshot.json +++ b/mobile/rpc-foundation/goldens/diff-review-snapshot.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/diff-review-status-unavailable.json b/mobile/rpc-foundation/goldens/diff-review-status-unavailable.json index 2e3ed0a70f0..0a9c707944b 100644 --- a/mobile/rpc-foundation/goldens/diff-review-status-unavailable.json +++ b/mobile/rpc-foundation/goldens/diff-review-status-unavailable.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/diff-review-worktree-file-diff.json b/mobile/rpc-foundation/goldens/diff-review-worktree-file-diff.json index 670bb312449..9803138546f 100644 --- a/mobile/rpc-foundation/goldens/diff-review-worktree-file-diff.json +++ b/mobile/rpc-foundation/goldens/diff-review-worktree-file-diff.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/file-tap-open-refused.json b/mobile/rpc-foundation/goldens/file-tap-open-refused.json index cc0555b1f91..96a52c5a0ec 100644 --- a/mobile/rpc-foundation/goldens/file-tap-open-refused.json +++ b/mobile/rpc-foundation/goldens/file-tap-open-refused.json @@ -3,7 +3,7 @@ "family": "files.terminal-path-tap", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e20a76ecd5e820dc4797fb25307810af68b5ced996dba2b9599464a21b5cbe1b", diff --git a/mobile/rpc-foundation/goldens/file-tap-opens-worktree-file.json b/mobile/rpc-foundation/goldens/file-tap-opens-worktree-file.json index 164436d189c..9460ecdad88 100644 --- a/mobile/rpc-foundation/goldens/file-tap-opens-worktree-file.json +++ b/mobile/rpc-foundation/goldens/file-tap-opens-worktree-file.json @@ -3,7 +3,7 @@ "family": "files.terminal-path-tap", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e20a76ecd5e820dc4797fb25307810af68b5ced996dba2b9599464a21b5cbe1b", diff --git a/mobile/rpc-foundation/goldens/file-tap-previews-absolute-artifact.json b/mobile/rpc-foundation/goldens/file-tap-previews-absolute-artifact.json index 68400bc1aaf..51af13c9b6a 100644 --- a/mobile/rpc-foundation/goldens/file-tap-previews-absolute-artifact.json +++ b/mobile/rpc-foundation/goldens/file-tap-previews-absolute-artifact.json @@ -3,7 +3,7 @@ "family": "files.terminal-path-tap", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e20a76ecd5e820dc4797fb25307810af68b5ced996dba2b9599464a21b5cbe1b", diff --git a/mobile/rpc-foundation/goldens/file-tap-resolve-miss.json b/mobile/rpc-foundation/goldens/file-tap-resolve-miss.json index f5bbcdbffc8..c57d44a47fd 100644 --- a/mobile/rpc-foundation/goldens/file-tap-resolve-miss.json +++ b/mobile/rpc-foundation/goldens/file-tap-resolve-miss.json @@ -3,7 +3,7 @@ "family": "files.terminal-path-tap", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e20a76ecd5e820dc4797fb25307810af68b5ced996dba2b9599464a21b5cbe1b", diff --git a/mobile/rpc-foundation/goldens/file-tap-resolve-refused.json b/mobile/rpc-foundation/goldens/file-tap-resolve-refused.json index 5890276c472..e0782e72028 100644 --- a/mobile/rpc-foundation/goldens/file-tap-resolve-refused.json +++ b/mobile/rpc-foundation/goldens/file-tap-resolve-refused.json @@ -3,7 +3,7 @@ "family": "files.terminal-path-tap", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e20a76ecd5e820dc4797fb25307810af68b5ced996dba2b9599464a21b5cbe1b", diff --git a/mobile/rpc-foundation/goldens/files-explorer-legacy-fallback.json b/mobile/rpc-foundation/goldens/files-explorer-legacy-fallback.json index 3bcac330cd1..bc8949b6ddc 100644 --- a/mobile/rpc-foundation/goldens/files-explorer-legacy-fallback.json +++ b/mobile/rpc-foundation/goldens/files-explorer-legacy-fallback.json @@ -3,7 +3,7 @@ "family": "files.explorer-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7a42a348f4407b94cf75ac77a4b6b783b7d28103b1ae1d111d2708dab0dd4a0c", diff --git a/mobile/rpc-foundation/goldens/files-explorer-readdir.json b/mobile/rpc-foundation/goldens/files-explorer-readdir.json index 8cd7d414207..3635407b85e 100644 --- a/mobile/rpc-foundation/goldens/files-explorer-readdir.json +++ b/mobile/rpc-foundation/goldens/files-explorer-readdir.json @@ -3,7 +3,7 @@ "family": "files.explorer-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7a42a348f4407b94cf75ac77a4b6b783b7d28103b1ae1d111d2708dab0dd4a0c", diff --git a/mobile/rpc-foundation/goldens/files-ownership-local.json b/mobile/rpc-foundation/goldens/files-ownership-local.json index 39c73196456..b6fee34f12f 100644 --- a/mobile/rpc-foundation/goldens/files-ownership-local.json +++ b/mobile/rpc-foundation/goldens/files-ownership-local.json @@ -3,7 +3,7 @@ "family": "files.mutation-ownership", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-ownership-ssh.json b/mobile/rpc-foundation/goldens/files-ownership-ssh.json index bbe1b7a7fce..1c7281ba01b 100644 --- a/mobile/rpc-foundation/goldens/files-ownership-ssh.json +++ b/mobile/rpc-foundation/goldens/files-ownership-ssh.json @@ -3,7 +3,7 @@ "family": "files.mutation-ownership", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-preview-artifact-direct.json b/mobile/rpc-foundation/goldens/files-preview-artifact-direct.json index 03709d3cb14..f51dbbd96b3 100644 --- a/mobile/rpc-foundation/goldens/files-preview-artifact-direct.json +++ b/mobile/rpc-foundation/goldens/files-preview-artifact-direct.json @@ -3,7 +3,7 @@ "family": "files.preview-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-preview-artifact-image-read.json b/mobile/rpc-foundation/goldens/files-preview-artifact-image-read.json new file mode 100644 index 00000000000..d656c291e6b --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-preview-artifact-image-read.json @@ -0,0 +1,95 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-artifact-image", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "39fe4bce80a2323dbc276235daab48e7fe55d1fdb9c1ea1eeac7c17bceb8df18", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "7659b8b575da": { + "preview": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "b5608d3d00c8": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifactPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/shot.png\",\"grantId\":\"grant-1\"}}" + }, + "ce511c0ab8ac": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + } + }, + "recording": { + "scenario": "files-preview-artifact-image-read", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["ce511c0ab8ac"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "eee847a9d90d" + }, + "state": "7659b8b575da", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-preview-artifact-image.json b/mobile/rpc-foundation/goldens/files-preview-artifact-image.json index 11004a55d31..8c11d914f1d 100644 --- a/mobile/rpc-foundation/goldens/files-preview-artifact-image.json +++ b/mobile/rpc-foundation/goldens/files-preview-artifact-image.json @@ -3,7 +3,7 @@ "family": "files.preview-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-preview-grant-refresh.json b/mobile/rpc-foundation/goldens/files-preview-grant-refresh.json index 92fb6a71365..a2643397928 100644 --- a/mobile/rpc-foundation/goldens/files-preview-grant-refresh.json +++ b/mobile/rpc-foundation/goldens/files-preview-grant-refresh.json @@ -3,7 +3,7 @@ "family": "files.preview-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-preview-worktree-image-read.json b/mobile/rpc-foundation/goldens/files-preview-worktree-image-read.json new file mode 100644 index 00000000000..3a934f629fe --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-preview-worktree-image-read.json @@ -0,0 +1,94 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-worktree-image", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "767af05bb58756f4adc960b95ee6547cc28145c7f6c7c6a4b27582aed9741e37", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "70f782fa6ef2": { + "name": "files.readPreview#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/logo.png\"}}" + }, + "7659b8b575da": { + "preview": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "a568af21f540": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + } + }, + "recording": { + "scenario": "files-preview-worktree-image-read", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["a568af21f540"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "eee847a9d90d" + }, + "state": "7659b8b575da", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-preview-worktree-image.json b/mobile/rpc-foundation/goldens/files-preview-worktree-image.json index 790d0a8416d..6205702638e 100644 --- a/mobile/rpc-foundation/goldens/files-preview-worktree-image.json +++ b/mobile/rpc-foundation/goldens/files-preview-worktree-image.json @@ -3,7 +3,7 @@ "family": "files.preview-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-preview-worktree-text-read.json b/mobile/rpc-foundation/goldens/files-preview-worktree-text-read.json new file mode 100644 index 00000000000..e89fb7239b5 --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-preview-worktree-text-read.json @@ -0,0 +1,97 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-worktree-text", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "21c3c61a18939873157f74c8d2d28bceae01abc702565af1ca897ab7304cb93c", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1cf4496bc8c0": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 8, + "content": "# readme", + "truncated": false + } + } + } + }, + "3f8bf3069e3d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 8, + "content": "# readme", + "kind": "markdown", + "status": "ready", + "truncated": false + } + }, + "47ef2e397e18": { + "preview": { + "byteLength": 8, + "content": "# readme", + "kind": "markdown", + "status": "ready", + "truncated": false + } + }, + "8423ff93fda2": { + "name": "files.read#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + } + }, + "recording": { + "scenario": "files-preview-worktree-text-read", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["1cf4496bc8c0"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "3f8bf3069e3d" + }, + "state": "47ef2e397e18", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-preview-worktree.json b/mobile/rpc-foundation/goldens/files-preview-worktree.json index dff7b0b3e9c..285dd5b2070 100644 --- a/mobile/rpc-foundation/goldens/files-preview-worktree.json +++ b/mobile/rpc-foundation/goldens/files-preview-worktree.json @@ -3,7 +3,7 @@ "family": "files.preview-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-save-blind.json b/mobile/rpc-foundation/goldens/files-save-blind.json index a197e99902d..5a3abcf56fd 100644 --- a/mobile/rpc-foundation/goldens/files-save-blind.json +++ b/mobile/rpc-foundation/goldens/files-save-blind.json @@ -3,7 +3,7 @@ "family": "files.preview-save", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-save-verified.json b/mobile/rpc-foundation/goldens/files-save-verified.json index e91ead6d80f..0421a3cba2b 100644 --- a/mobile/rpc-foundation/goldens/files-save-verified.json +++ b/mobile/rpc-foundation/goldens/files-save-verified.json @@ -3,7 +3,7 @@ "family": "files.preview-save", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-tab-doc-shapes.json b/mobile/rpc-foundation/goldens/files-tab-doc-shapes.json index ca757e6344a..a6a119288df 100644 --- a/mobile/rpc-foundation/goldens/files-tab-doc-shapes.json +++ b/mobile/rpc-foundation/goldens/files-tab-doc-shapes.json @@ -3,7 +3,7 @@ "family": "files.tab-doc", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/home-host-accounts.json b/mobile/rpc-foundation/goldens/home-host-accounts.json index dd399a534da..8206ebce25d 100644 --- a/mobile/rpc-foundation/goldens/home-host-accounts.json +++ b/mobile/rpc-foundation/goldens/home-host-accounts.json @@ -3,7 +3,7 @@ "family": "home.host-accounts", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c632fdbc4b730777ecb09f08bec14cca0586042b01ed99d40d0228806c7def4a", diff --git a/mobile/rpc-foundation/goldens/home-host-stats.json b/mobile/rpc-foundation/goldens/home-host-stats.json index f2c1f9b32c7..1e17e46e1ab 100644 --- a/mobile/rpc-foundation/goldens/home-host-stats.json +++ b/mobile/rpc-foundation/goldens/home-host-stats.json @@ -3,7 +3,7 @@ "family": "home.host-stats", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", diff --git a/mobile/rpc-foundation/goldens/host-view-settings-sync.json b/mobile/rpc-foundation/goldens/host-view-settings-sync.json index 96dd5de9457..b63a50b9e0b 100644 --- a/mobile/rpc-foundation/goldens/host-view-settings-sync.json +++ b/mobile/rpc-foundation/goldens/host-view-settings-sync.json @@ -3,7 +3,7 @@ "family": "host.view-settings", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", diff --git a/mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json b/mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json index 13890d52477..7bfa1f93583 100644 --- a/mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json +++ b/mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json @@ -3,7 +3,7 @@ "family": "host.worktree-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", diff --git a/mobile/rpc-foundation/goldens/host-worktree-delete-refused.json b/mobile/rpc-foundation/goldens/host-worktree-delete-refused.json index f6fbefe7a73..52fd008c50c 100644 --- a/mobile/rpc-foundation/goldens/host-worktree-delete-refused.json +++ b/mobile/rpc-foundation/goldens/host-worktree-delete-refused.json @@ -3,7 +3,7 @@ "family": "host.worktree-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", diff --git a/mobile/rpc-foundation/goldens/host-worktree-refresh-stream.json b/mobile/rpc-foundation/goldens/host-worktree-refresh-stream.json index efa04b4a345..9d28e5e2ebd 100644 --- a/mobile/rpc-foundation/goldens/host-worktree-refresh-stream.json +++ b/mobile/rpc-foundation/goldens/host-worktree-refresh-stream.json @@ -3,7 +3,7 @@ "family": "host-worktree-refresh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json b/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json index 80a7ecb0117..1634c69eff3 100644 --- a/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json +++ b/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json b/mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json index 658f7e5cf2d..0de13f9f0b6 100644 --- a/mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json +++ b/mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/inventory-lifecycle.json b/mobile/rpc-foundation/goldens/inventory-lifecycle.json index c60d4fb16df..9c65d9e03f8 100644 --- a/mobile/rpc-foundation/goldens/inventory-lifecycle.json +++ b/mobile/rpc-foundation/goldens/inventory-lifecycle.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/inventory-repeat-query.json b/mobile/rpc-foundation/goldens/inventory-repeat-query.json index 8eec5ac7368..55e705fe802 100644 --- a/mobile/rpc-foundation/goldens/inventory-repeat-query.json +++ b/mobile/rpc-foundation/goldens/inventory-repeat-query.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/lifecycle-b3.json b/mobile/rpc-foundation/goldens/lifecycle-b3.json index b7c7c228541..7faa0707243 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-b3.json +++ b/mobile/rpc-foundation/goldens/lifecycle-b3.json @@ -3,7 +3,7 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json b/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json index b504be2692d..792d8d69e95 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json +++ b/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json b/mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json index 5be84f3dc70..e6711bde5a0 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json +++ b/mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json b/mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json index 70dbfd9adad..d4b5bac6b88 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json +++ b/mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json b/mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json index 9c46f0529bd..ca3b5b30a64 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json +++ b/mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/linear-select-workspace.json b/mobile/rpc-foundation/goldens/linear-select-workspace.json index 88e10a16286..a2bb58f3661 100644 --- a/mobile/rpc-foundation/goldens/linear-select-workspace.json +++ b/mobile/rpc-foundation/goldens/linear-select-workspace.json @@ -3,7 +3,7 @@ "family": "linear.select-workspace-picker", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b65996c152b632d553a31e07e31ea5c76f998eada42cf0966923d730da21908f", diff --git a/mobile/rpc-foundation/goldens/live-worktree-name-stream.json b/mobile/rpc-foundation/goldens/live-worktree-name-stream.json index 64803c61faf..9bff3a4e51b 100644 --- a/mobile/rpc-foundation/goldens/live-worktree-name-stream.json +++ b/mobile/rpc-foundation/goldens/live-worktree-name-stream.json @@ -3,7 +3,7 @@ "family": "live-worktree-name", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.create-1.json b/mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.create-1.json index 393e1e6a3dd..6f0f60122d4 100644 --- a/mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.create-1.json +++ b/mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.create-1.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.createsupport-1.json b/mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.createsupport-1.json index f2932f48104..37896453de4 100644 --- a/mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.createsupport-1.json +++ b/mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.createsupport-1.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/matrix-agentsession.structured-launch-agentsession.createsupport-1.json b/mobile/rpc-foundation/goldens/matrix-agentsession.structured-launch-agentsession.createsupport-1.json index f3c1dd31dd4..85653cf50d7 100644 --- a/mobile/rpc-foundation/goldens/matrix-agentsession.structured-launch-agentsession.createsupport-1.json +++ b/mobile/rpc-foundation/goldens/matrix-agentsession.structured-launch-agentsession.createsupport-1.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/matrix-aivault.history-aivault.listsessions-1.json b/mobile/rpc-foundation/goldens/matrix-aivault.history-aivault.listsessions-1.json index 9dd00581b43..201a59ce8c7 100644 --- a/mobile/rpc-foundation/goldens/matrix-aivault.history-aivault.listsessions-1.json +++ b/mobile/rpc-foundation/goldens/matrix-aivault.history-aivault.listsessions-1.json @@ -3,7 +3,7 @@ "family": "aiVault.history", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7add46922ba5486e56c8acd99a53d083b605f5d98f3cc44ee3cb350ec0406080", @@ -86,18 +86,6 @@ } } }, - "1277d47c0e64": { - "activeWorktreePath": "/repo/feature", - "hostStatusResult": { - "$rpc": "null" - }, - "refreshing": false, - "scope": "workspace", - "screenState": { - "kind": "error", - "message": "Cannot read properties of undefined (reading 'sessions')" - } - }, "23523c413cbd": { "activeWorktreePath": "/repo/feature", "hostStatusResult": { @@ -297,18 +285,6 @@ } } }, - "61f76365e23c": { - "activeWorktreePath": "/repo/feature", - "hostStatusResult": { - "$rpc": "null" - }, - "refreshing": false, - "scope": "workspace", - "screenState": { - "kind": "error", - "message": "Cannot read properties of null (reading 'sessions')" - } - }, "698f848e6967": { "activeWorktreePath": "/repo/feature", "hostStatusResult": { @@ -571,21 +547,16 @@ "$rpc": "undefined" } }, - "fc659419e768": { + "fead2d57ab18": { "activeWorktreePath": "/repo/feature", "hostStatusResult": { - "capabilities": ["aiVault.v1"] + "$rpc": "null" }, "refreshing": false, "scope": "workspace", "screenState": { - "issues": { - "$rpc": "undefined" - }, - "kind": "ready", - "sessions": { - "$rpc": "undefined" - } + "kind": "error", + "message": "The host sent a reply this app could not read (aiVault.listSessions)" } } }, @@ -612,7 +583,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "1277d47c0e64", + "state": "fead2d57ab18", "effects": [] } }, @@ -624,7 +595,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "61f76365e23c", + "state": "fead2d57ab18", "effects": [] } }, @@ -636,7 +607,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "fc659419e768", + "state": "fead2d57ab18", "effects": [] } }, @@ -648,7 +619,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "fc659419e768", + "state": "fead2d57ab18", "effects": [] } }, @@ -660,7 +631,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "fc659419e768", + "state": "fead2d57ab18", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-platform-status.json b/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-platform-status.json index df236c12865..55caa0ee5b1 100644 --- a/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-platform-status.json +++ b/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-platform-status.json @@ -3,7 +3,7 @@ "family": "aiVault.history-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3", diff --git a/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-status.get-2.json b/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-status.get-2.json index dc1e62e3ae1..68ca8a29cfd 100644 --- a/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-status.get-2.json +++ b/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-status.get-2.json @@ -3,7 +3,7 @@ "family": "aiVault.history-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3", @@ -371,27 +371,6 @@ } } }, - "5368af075169": { - "crash": { - "$rpc": "null" - }, - "elements": { - "ChevronLeft": 1, - "Pressable": 3, - "RefreshCw": 1, - "SafeAreaView": 1, - "Text": 5, - "View": 4 - }, - "labels": ["Back", "Refresh agent sessions"], - "text": [ - "Agent Session History", - "orca-history", - "Unable to Load", - "Cannot read properties of undefined (reading 'capabilities')", - "Retry" - ] - }, "6eacae1aa018": { "name": "status.get#2", "ordinal": 5, @@ -518,6 +497,27 @@ } } }, + "8cc4f2d607a6": { + "crash": { + "$rpc": "null" + }, + "elements": { + "ChevronLeft": 1, + "Pressable": 3, + "RefreshCw": 1, + "SafeAreaView": 1, + "Text": 5, + "View": 4 + }, + "labels": ["Back", "Refresh agent sessions"], + "text": [ + "Agent Session History", + "orca-history", + "Unable to Load", + "The host sent a reply this app could not read (status.get)", + "Retry" + ] + }, "8d930b47bf2f": { "name": "worktree.ps#1", "ordinal": 1, @@ -652,27 +652,6 @@ "Retry" ] }, - "c767ef059f7e": { - "crash": { - "$rpc": "null" - }, - "elements": { - "ChevronLeft": 1, - "Pressable": 3, - "RefreshCw": 1, - "SafeAreaView": 1, - "Text": 5, - "View": 4 - }, - "labels": ["Back", "Refresh agent sessions"], - "text": [ - "Agent Session History", - "orca-history", - "Unable to Load", - "Cannot read properties of null (reading 'capabilities')", - "Retry" - ] - }, "cee4e3edb0a1": { "name": "status.get#2", "ordinal": 6, @@ -775,7 +754,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "5368af075169", + "state": "8cc4f2d607a6", "effects": [] } }, @@ -787,7 +766,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "c767ef059f7e", + "state": "8cc4f2d607a6", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-worktree.ps-1.json index 8346115e3c3..b2abd8acd44 100644 --- a/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-worktree.ps-1.json +++ b/mobile/rpc-foundation/goldens/matrix-aivault.history-screen-worktree.ps-1.json @@ -3,7 +3,7 @@ "family": "aiVault.history-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3", diff --git a/mobile/rpc-foundation/goldens/matrix-aivault.history-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-aivault.history-status.get-1.json index 64cf096b31e..8053b35fc98 100644 --- a/mobile/rpc-foundation/goldens/matrix-aivault.history-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-aivault.history-status.get-1.json @@ -3,7 +3,7 @@ "family": "aiVault.history", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7add46922ba5486e56c8acd99a53d083b605f5d98f3cc44ee3cb350ec0406080", @@ -274,18 +274,6 @@ } } }, - "4af7915fce72": { - "activeWorktreePath": "/repo/feature", - "hostStatusResult": { - "$rpc": "null" - }, - "refreshing": false, - "scope": "workspace", - "screenState": { - "kind": "error", - "message": "Cannot read properties of undefined (reading 'capabilities')" - } - }, "698f848e6967": { "activeWorktreePath": "/repo/feature", "hostStatusResult": { @@ -429,18 +417,6 @@ "kind": "unsupported" } }, - "b35d53bd952d": { - "activeWorktreePath": "/repo/feature", - "hostStatusResult": { - "$rpc": "null" - }, - "refreshing": false, - "scope": "workspace", - "screenState": { - "kind": "error", - "message": "Cannot read properties of null (reading 'capabilities')" - } - }, "b799ba7b0c33": { "name": "status.get#1", "ordinal": 1, @@ -576,6 +552,18 @@ "$rpc": "undefined" } }, + "f79de442ffa2": { + "activeWorktreePath": "/repo/feature", + "hostStatusResult": { + "$rpc": "null" + }, + "refreshing": false, + "scope": "workspace", + "screenState": { + "kind": "error", + "message": "The host sent a reply this app could not read (status.get)" + } + }, "fd9ea98fae8e": { "activeWorktreePath": "/repo/feature", "hostStatusResult": { @@ -612,7 +600,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "4af7915fce72", + "state": "f79de442ffa2", "effects": [] } }, @@ -624,7 +612,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "b35d53bd952d", + "state": "f79de442ffa2", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-session.tabs.createterminal-1.json b/mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-session.tabs.createterminal-1.json index b71411a9a99..421223a8d7f 100644 --- a/mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-session.tabs.createterminal-1.json +++ b/mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-session.tabs.createterminal-1.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-terminal.send-1.json index a3c383d6260..2868c463b3d 100644 --- a/mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/matrix-aivault.resume-preparation-aivault.preparesessionresume-1.json b/mobile/rpc-foundation/goldens/matrix-aivault.resume-preparation-aivault.preparesessionresume-1.json index 376b39076d1..6f715931a9e 100644 --- a/mobile/rpc-foundation/goldens/matrix-aivault.resume-preparation-aivault.preparesessionresume-1.json +++ b/mobile/rpc-foundation/goldens/matrix-aivault.resume-preparation-aivault.preparesessionresume-1.json @@ -3,7 +3,7 @@ "family": "aiVault.resume-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.dialog-browser.dialogaccept-1.json b/mobile/rpc-foundation/goldens/matrix-browser.dialog-browser.dialogaccept-1.json index 0474daaeff7..6f893b8f5da 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.dialog-browser.dialogaccept-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.dialog-browser.dialogaccept-1.json @@ -3,7 +3,7 @@ "family": "browser.dialog", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keyboardinserttext-1.json b/mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keyboardinserttext-1.json index 52319902d66..2032bf4b1c4 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keyboardinserttext-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keyboardinserttext-1.json @@ -3,7 +3,7 @@ "family": "browser.keyboard", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keypress-1.json b/mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keypress-1.json index c734f497be5..7e582828527 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keypress-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keypress-1.json @@ -3,7 +3,7 @@ "family": "browser.keyboard", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseclick-1.json b/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseclick-1.json index a5d6b62036a..9253a936da4 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseclick-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseclick-1.json @@ -3,7 +3,7 @@ "family": "browser.pointer-click", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousedown-1.json b/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousedown-1.json index 4187b00765a..b7984e55abf 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousedown-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousedown-1.json @@ -3,7 +3,7 @@ "family": "browser.pointer-click", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousemove-1.json b/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousemove-1.json index 5c2e15369e1..9e6243891d2 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousemove-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousemove-1.json @@ -3,7 +3,7 @@ "family": "browser.pointer-click", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseup-1.json b/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseup-1.json index d2c1a83f73c..fb4d631b251 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseup-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseup-1.json @@ -3,7 +3,7 @@ "family": "browser.pointer-click", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousemove-1.json b/mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousemove-1.json index cab0bc52dfd..f151f3b3933 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousemove-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousemove-1.json @@ -3,7 +3,7 @@ "family": "browser.wheel", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousewheel-1.json b/mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousewheel-1.json index 0b568b0333f..93f0adb616f 100644 --- a/mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousewheel-1.json +++ b/mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousewheel-1.json @@ -3,7 +3,7 @@ "family": "browser.wheel", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/matrix-clipboard.image-attachment-clipboard.startimageupload-1.json b/mobile/rpc-foundation/goldens/matrix-clipboard.image-attachment-clipboard.startimageupload-1.json index e645eb883d8..b96753d8d24 100644 --- a/mobile/rpc-foundation/goldens/matrix-clipboard.image-attachment-clipboard.startimageupload-1.json +++ b/mobile/rpc-foundation/goldens/matrix-clipboard.image-attachment-clipboard.startimageupload-1.json @@ -3,7 +3,7 @@ "family": "clipboard.image-attachment", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.saveimageastempfile-1.json b/mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.saveimageastempfile-1.json index c88db1965ed..6e7ac708fe1 100644 --- a/mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.saveimageastempfile-1.json +++ b/mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.saveimageastempfile-1.json @@ -3,7 +3,7 @@ "family": "clipboard.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.startimageupload-1.json b/mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.startimageupload-1.json index 81e9c3b9f9e..6bf519616f7 100644 --- a/mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.startimageupload-1.json +++ b/mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.startimageupload-1.json @@ -3,7 +3,7 @@ "family": "clipboard.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json index e7a96457e17..e1d74ee4acd 100644 --- a/mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json @@ -3,7 +3,7 @@ "family": "components.codex-reset-capability", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/matrix-components.codex-reset-credit-accounts.consumecodexresetcredit-1.json b/mobile/rpc-foundation/goldens/matrix-components.codex-reset-credit-accounts.consumecodexresetcredit-1.json index a8587ad46ef..775bd039507 100644 --- a/mobile/rpc-foundation/goldens/matrix-components.codex-reset-credit-accounts.consumecodexresetcredit-1.json +++ b/mobile/rpc-foundation/goldens/matrix-components.codex-reset-credit-accounts.consumecodexresetcredit-1.json @@ -3,7 +3,7 @@ "family": "components.codex-reset-credit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "76b53dea504a688493843e23e8e7d052fc196f559bb1e2579d0fd38bf3156d0e", diff --git a/mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json b/mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json index 02f15783954..c4d2cb8b35b 100644 --- a/mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json @@ -3,7 +3,7 @@ "family": "components.execution-target-local", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json b/mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json index b0e1700b6a0..af2f982f16d 100644 --- a/mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json @@ -3,7 +3,7 @@ "family": "components.execution-target", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json b/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json index cbd0ed8fa86..d83cf728358 100644 --- a/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json +++ b/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json @@ -3,7 +3,7 @@ "family": "components.execution-target", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json b/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json index 65fd3e7e31a..582bcf99d25 100644 --- a/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json @@ -3,7 +3,7 @@ "family": "components.execution-target", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/matrix-components.new-workspace-repositories-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-components.new-workspace-repositories-repo.list-1.json index 0fdba3fbe0c..d62336945f6 100644 --- a/mobile/rpc-foundation/goldens/matrix-components.new-workspace-repositories-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-components.new-workspace-repositories-repo.list-1.json @@ -3,7 +3,7 @@ "family": "components.new-workspace-repositories", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "64c1772f0f95a3c43fbb14398a8804b2b4784f7f18874e4fd79767ae634c7faa", diff --git a/mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json b/mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json index 2ceb8f8aa26..efb3484c637 100644 --- a/mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json @@ -3,7 +3,7 @@ "family": "components.setup-script", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.list-1.json b/mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.list-1.json index 7295e403898..89801f19313 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.list-1.json @@ -3,7 +3,7 @@ "family": "files.explorer-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7a42a348f4407b94cf75ac77a4b6b783b7d28103b1ae1d111d2708dab0dd4a0c", @@ -417,26 +417,6 @@ "ordinal": 2, "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readDir\",\"params\":{\"worktree\":\"id:wt-files\",\"relativePath\":\"\"}}" }, - "8966ebfaf515": { - "crash": { - "$rpc": "null" - }, - "elements": { - "ChevronLeft": 1, - "Pressable": 2, - "SafeAreaView": 1, - "Text": 4, - "View": 4 - }, - "labels": ["Back to session"], - "rows": [], - "text": [ - "Files", - "orca-files", - "Cannot read properties of undefined (reading 'files')", - "Retry" - ] - }, "a0139a06ef98": { "crash": { "$rpc": "null" @@ -452,36 +432,6 @@ "rows": [], "text": ["Files", "orca-files", "No files found"] }, - "aa33782c6235": { - "crash": { - "$rpc": "null" - }, - "elements": { - "ChevronLeft": 1, - "Pressable": 2, - "SafeAreaView": 1, - "Text": 4, - "View": 4 - }, - "labels": ["Back to session"], - "rows": [], - "text": ["Files", "orca-files", "Cannot read properties of null (reading 'files')", "Retry"] - }, - "acc0ab029cee": { - "crash": { - "$rpc": "null" - }, - "elements": { - "ChevronLeft": 1, - "Pressable": 2, - "SafeAreaView": 1, - "Text": 4, - "View": 4 - }, - "labels": ["Back to session"], - "rows": [], - "text": ["Files", "orca-files", "files is not iterable", "Retry"] - }, "b85511fb8929": { "crash": { "$rpc": "null" @@ -591,6 +541,26 @@ "$rpc": "undefined" } }, + "eededbc093fb": { + "crash": { + "$rpc": "null" + }, + "elements": { + "ChevronLeft": 1, + "Pressable": 2, + "SafeAreaView": 1, + "Text": 4, + "View": 4 + }, + "labels": ["Back to session"], + "rows": [], + "text": [ + "Files", + "orca-files", + "The host sent a reply this app could not read (files.list)", + "Retry" + ] + }, "f9892850cc0f": { "name": "files.list#1", "ordinal": 3, @@ -659,7 +629,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "8966ebfaf515", + "state": "eededbc093fb", "effects": [] } }, @@ -671,7 +641,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "aa33782c6235", + "state": "eededbc093fb", "effects": [] } }, @@ -683,7 +653,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "acc0ab029cee", + "state": "eededbc093fb", "effects": [] } }, @@ -695,7 +665,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "acc0ab029cee", + "state": "eededbc093fb", "effects": [] } }, @@ -707,7 +677,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "acc0ab029cee", + "state": "eededbc093fb", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.readdir-1.json b/mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.readdir-1.json index 63cc8bd0f7c..f6922f9ddcc 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.readdir-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.readdir-1.json @@ -3,7 +3,7 @@ "family": "files.explorer-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7a42a348f4407b94cf75ac77a4b6b783b7d28103b1ae1d111d2708dab0dd4a0c", @@ -359,6 +359,26 @@ } } }, + "6c25da20f340": { + "crash": { + "$rpc": "null" + }, + "elements": { + "ChevronLeft": 1, + "Pressable": 2, + "SafeAreaView": 1, + "Text": 4, + "View": 4 + }, + "labels": ["Back to session"], + "rows": [], + "text": [ + "Files", + "orca-files", + "The host sent a reply this app could not read (files.readDir)", + "Retry" + ] + }, "7135933422f5": { "name": "files.readDir#1", "ordinal": 1, @@ -506,13 +526,6 @@ } } }, - "bcba3c565d8e": { - "name": "screen.crash", - "ordinal": 3, - "value": { - "message": "entries.filter is not a function" - } - }, "e0b32411ccef": { "name": "files.readDir#1", "ordinal": 1, @@ -562,13 +575,6 @@ "rows": [], "text": ["Files", "orca-files"] }, - "ea2c96b08e6b": { - "crash": "entries.filter is not a function", - "elements": {}, - "labels": [], - "rows": [], - "text": [] - }, "eb79a9b3682a": { "status": "fulfilled", "startedAt": 0, @@ -655,7 +661,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "a0139a06ef98", + "state": "6c25da20f340", "effects": [] } }, @@ -667,7 +673,7 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "a0139a06ef98", + "state": "6c25da20f340", "effects": [] } }, @@ -679,8 +685,8 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "ea2c96b08e6b", - "effects": ["bcba3c565d8e"] + "state": "6c25da20f340", + "effects": [] } }, { @@ -691,8 +697,8 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "ea2c96b08e6b", - "effects": ["bcba3c565d8e"] + "state": "6c25da20f340", + "effects": [] } }, { @@ -703,8 +709,8 @@ "settlements": { "mount": "eb79a9b3682a" }, - "state": "ea2c96b08e6b", - "effects": ["bcba3c565d8e"] + "state": "6c25da20f340", + "effects": [] } }, { diff --git a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json index 61fea3f8ed5..026288fd2cd 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json @@ -3,7 +3,7 @@ "family": "files.mutation-ownership", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -353,6 +353,17 @@ } } }, + "c4c200b5bf69": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (ssh.getState)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "c7584e82c72f": { "status": "rejected", "startedAt": 0, @@ -400,16 +411,6 @@ "ordinal": 4, "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:workspace-1\"}}" }, - "ce2b29907ae3": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of null (reading 'state')", - "isRpcDeliveryUnknown": false - } - }, "d08f74d65ee6": { "name": "status.get#1", "ordinal": 2, @@ -452,16 +453,6 @@ } } }, - "d954a0a142a5": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of undefined (reading 'state')", - "isRpcDeliveryUnknown": false - } - }, "e6c4b2665e65": { "name": "ssh.getState#1", "ordinal": 5, @@ -644,7 +635,7 @@ "sender": ["e81d3a627ac2", "3cf4416a7928", "0622950ee1c9"], "payloads": ["d08f74d65ee6", "c924e7a5a7da", "088efd8ff3f7"], "settlements": { - "capture": "d954a0a142a5" + "capture": "c4c200b5bf69" }, "state": "518ec57c381a", "effects": [] @@ -656,7 +647,7 @@ "sender": ["e81d3a627ac2", "3cf4416a7928", "7fb21c6984cd"], "payloads": ["d08f74d65ee6", "c924e7a5a7da", "088efd8ff3f7"], "settlements": { - "capture": "ce2b29907ae3" + "capture": "c4c200b5bf69" }, "state": "518ec57c381a", "effects": [] diff --git a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json index 640e691bd42..723f06a7683 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json @@ -3,7 +3,7 @@ "family": "files.mutation-ownership", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json index 7598f9f0ce5..81bbf599592 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json @@ -3,7 +3,7 @@ "family": "files.mutation-ownership", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -13,6 +13,17 @@ "projectionVersion": 2, "goldenFormatVersion": 5, "values": { + "0233661d0a29": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (worktree.show)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "088efd8ff3f7": { "name": "ssh.getState#1", "ordinal": 6, @@ -89,16 +100,6 @@ } } }, - "2588fd63a157": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of undefined (reading 'worktree')", - "isRpcDeliveryUnknown": false - } - }, "29bfbe94cca9": { "status": "fulfilled", "startedAt": 0, @@ -443,16 +444,6 @@ "isRpcDeliveryUnknown": true } }, - "b5447f4dd931": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of null (reading 'worktree')", - "isRpcDeliveryUnknown": false - } - }, "b6e8ae2b152e": { "name": "ssh.getState#1", "ordinal": 5, @@ -644,7 +635,7 @@ "sender": ["e81d3a627ac2", "2edf2dc7f524"], "payloads": ["d08f74d65ee6", "c924e7a5a7da"], "settlements": { - "capture": "2588fd63a157" + "capture": "0233661d0a29" }, "state": "518ec57c381a", "effects": [] @@ -656,7 +647,7 @@ "sender": ["e81d3a627ac2", "c8d8aec2ecbb"], "payloads": ["d08f74d65ee6", "c924e7a5a7da"], "settlements": { - "capture": "b5447f4dd931" + "capture": "0233661d0a29" }, "state": "518ec57c381a", "effects": [] diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-artifact-image-files.readterminalartifactpreview-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-artifact-image-files.readterminalartifactpreview-1.json new file mode 100644 index 00000000000..4e192f50fe9 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-artifact-image-files.readterminalartifactpreview-1.json @@ -0,0 +1,643 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-artifact-image", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "83cad9595793b30d38115ba7118ae1400ecb2af21327853183e91289f4f33a04", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "011a73e13887": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "0254bd812679": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "0639efe16769": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Binary preview unavailable", + "reconnect": false, + "status": "error" + } + }, + "06ce305bc756": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "15467bba2d60": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "491897d7dd5d": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "645c5754be42": { + "preview": "unloaded" + }, + "6c3ad19b3a5a": { + "preview": { + "message": "Binary preview unavailable", + "reconnect": false, + "status": "error" + } + }, + "75ebb08fd335": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "7659b8b575da": { + "preview": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "879996a2a9e7": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b5608d3d00c8": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifactPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/shot.png\",\"grantId\":\"grant-1\"}}" + }, + "b96f3d16bdfc": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "bf5d986bfb2b": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "ce511c0ab8ac": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "d3fa8c5b05e4": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "dd9af848f659": { + "name": "files.readTerminalArtifactPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "eed3c09414c4": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.readTerminalArtifactPreview)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "fba5e3c89244": { + "preview": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + } + }, + "recording": { + "scenario": "matrix-files.preview-artifact-image-files.readterminalartifactpreview-1", + "checkpoints": [ + { + "id": "files-preview-artifact-image-read.normal:settled", + "observation": { + "sender": ["ce511c0ab8ac"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "eee847a9d90d" + }, + "state": "7659b8b575da", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.result-absent:settled", + "observation": { + "sender": ["dd9af848f659"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "eed3c09414c4" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.result-null:settled", + "observation": { + "sender": ["011a73e13887"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "eed3c09414c4" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.inner-ok-missing:settled", + "observation": { + "sender": ["06ce305bc756"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "0639efe16769" + }, + "state": "6c3ad19b3a5a", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.inner-false-string-error:settled", + "observation": { + "sender": ["bf5d986bfb2b"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "0639efe16769" + }, + "state": "6c3ad19b3a5a", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.inner-false-object-error:settled", + "observation": { + "sender": ["b96f3d16bdfc"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "0639efe16769" + }, + "state": "6c3ad19b3a5a", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.outer-refused:settled", + "observation": { + "sender": ["75ebb08fd335"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.outer-refused-no-message:settled", + "observation": { + "sender": ["d3fa8c5b05e4"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.method-not-found:settled", + "observation": { + "sender": ["491897d7dd5d"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.transport-rejection:settled", + "observation": { + "sender": ["0254bd812679"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "a947768bc0ed" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-artifact-image-read.transport-rejection-no-message:settled", + "observation": { + "sender": ["879996a2a9e7"], + "payloads": ["b5608d3d00c8"], + "settlements": { + "load": "c7584e82c72f" + }, + "state": "645c5754be42", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json index 74d5ad6eecf..24f83afdffb 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json @@ -3,7 +3,7 @@ "family": "files.preview-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -62,6 +62,17 @@ "status": "error" } }, + "2198677d4b7f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.readTerminalArtifact)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "2cbe3d82fe7d": { "name": "files.readTerminalArtifact#1", "ordinal": 1, @@ -543,9 +554,9 @@ "sender": ["359261481665"], "payloads": ["5beb3ae90517"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": [] } }, @@ -555,9 +566,9 @@ "sender": ["fc8164cc9095"], "payloads": ["5beb3ae90517"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": [] } }, @@ -567,9 +578,9 @@ "sender": ["6eb653749642"], "payloads": ["5beb3ae90517"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": [] } }, @@ -579,9 +590,9 @@ "sender": ["ce7d56dd9080"], "payloads": ["5beb3ae90517"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": [] } }, @@ -591,9 +602,9 @@ "sender": ["0181c916a00b"], "payloads": ["5beb3ae90517"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json index 4bc240f0d77..be9e5945688 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json @@ -3,7 +3,7 @@ "family": "files.preview-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -60,6 +60,17 @@ "status": "error" } }, + "2198677d4b7f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.readTerminalArtifact)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "29e625f367a7": { "name": "files.resolveTerminalPath#1", "ordinal": 3, @@ -646,9 +657,9 @@ "sender": ["0ea42bf424ad", "29e625f367a7", "bdf8e4e0083f"], "payloads": ["5beb3ae90517", "3fa46af4bb15", "679de887534f"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": ["ddb7adf79b98"] } }, @@ -658,9 +669,9 @@ "sender": ["0ea42bf424ad", "29e625f367a7", "842035a85a1c"], "payloads": ["5beb3ae90517", "3fa46af4bb15", "679de887534f"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": ["ddb7adf79b98"] } }, @@ -670,9 +681,9 @@ "sender": ["0ea42bf424ad", "29e625f367a7", "f24fc15f029f"], "payloads": ["5beb3ae90517", "3fa46af4bb15", "679de887534f"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": ["ddb7adf79b98"] } }, @@ -682,9 +693,9 @@ "sender": ["0ea42bf424ad", "29e625f367a7", "dc6e0288b015"], "payloads": ["5beb3ae90517", "3fa46af4bb15", "679de887534f"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": ["ddb7adf79b98"] } }, @@ -694,9 +705,9 @@ "sender": ["0ea42bf424ad", "29e625f367a7", "503d66d8b67a"], "payloads": ["5beb3ae90517", "3fa46af4bb15", "679de887534f"], "settlements": { - "load": "15467bba2d60" + "load": "2198677d4b7f" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": ["ddb7adf79b98"] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json index b41ab45fc7e..1b07ca8e439 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json @@ -3,7 +3,7 @@ "family": "files.preview-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -343,6 +343,17 @@ "truncated": false } }, + "54c8d9c728f2": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.resolveTerminalPath)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "5beb3ae90517": { "name": "files.readTerminalArtifact#1", "ordinal": 2, @@ -656,9 +667,9 @@ "sender": ["0ea42bf424ad", "ad1560697ac8"], "payloads": ["5beb3ae90517", "3fa46af4bb15"], "settlements": { - "load": "15467bba2d60" + "load": "54c8d9c728f2" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": [] } }, @@ -668,9 +679,9 @@ "sender": ["0ea42bf424ad", "09ce12d7b4ef"], "payloads": ["5beb3ae90517", "3fa46af4bb15"], "settlements": { - "load": "15467bba2d60" + "load": "54c8d9c728f2" }, - "state": "fba5e3c89244", + "state": "645c5754be42", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json index 56a83159526..810068a90fe 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json @@ -3,7 +3,7 @@ "family": "files.preview-save", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -62,6 +62,17 @@ "status": "error" } }, + "2198677d4b7f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.readTerminalArtifact)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "24ffb7059792": { "name": "files.readTerminalArtifact#1", "ordinal": 1, @@ -577,9 +588,9 @@ "sender": ["359261481665"], "payloads": ["5beb3ae90517"], "settlements": { - "save": "15467bba2d60" + "save": "2198677d4b7f" }, - "state": "5ac141a87b5a", + "state": "935100df69e4", "effects": [] } }, @@ -589,9 +600,9 @@ "sender": ["fc8164cc9095"], "payloads": ["5beb3ae90517"], "settlements": { - "save": "15467bba2d60" + "save": "2198677d4b7f" }, - "state": "5ac141a87b5a", + "state": "935100df69e4", "effects": [] } }, @@ -601,9 +612,9 @@ "sender": ["6eb653749642"], "payloads": ["5beb3ae90517"], "settlements": { - "save": "15467bba2d60" + "save": "2198677d4b7f" }, - "state": "5ac141a87b5a", + "state": "935100df69e4", "effects": [] } }, @@ -613,9 +624,9 @@ "sender": ["ce7d56dd9080"], "payloads": ["5beb3ae90517"], "settlements": { - "save": "15467bba2d60" + "save": "2198677d4b7f" }, - "state": "5ac141a87b5a", + "state": "935100df69e4", "effects": [] } }, @@ -625,9 +636,9 @@ "sender": ["0181c916a00b"], "payloads": ["5beb3ae90517"], "settlements": { - "save": "15467bba2d60" + "save": "2198677d4b7f" }, - "state": "5ac141a87b5a", + "state": "935100df69e4", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json index dde3b94fc13..6d342cac764 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json @@ -3,7 +3,7 @@ "family": "files.preview-save", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-worktree-image-files.readpreview-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-worktree-image-files.readpreview-1.json new file mode 100644 index 00000000000..abff758344f --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-worktree-image-files.readpreview-1.json @@ -0,0 +1,632 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-worktree-image", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "0962cb776492001a60db2ca89d715faec94c648962f140077fc9a85355784d81", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0639efe16769": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Binary preview unavailable", + "reconnect": false, + "status": "error" + } + }, + "15467bba2d60": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "211cdb1558da": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "54071f66c6d8": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "56177906925a": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "5c71a0c5523d": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "645c5754be42": { + "preview": "unloaded" + }, + "67dbd4c82715": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "6af2188434b2": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "6c3ad19b3a5a": { + "preview": { + "message": "Binary preview unavailable", + "reconnect": false, + "status": "error" + } + }, + "70f782fa6ef2": { + "name": "files.readPreview#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/logo.png\"}}" + }, + "7659b8b575da": { + "preview": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "79a49cf80ecf": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.readPreview)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, + "a51f35ec407c": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "a568af21f540": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "af09828e5413": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "c4730e0ddd60": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "ee977c2d3628": { + "name": "files.readPreview#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "fba5e3c89244": { + "preview": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + } + }, + "recording": { + "scenario": "matrix-files.preview-worktree-image-files.readpreview-1", + "checkpoints": [ + { + "id": "files-preview-worktree-image-read.normal:settled", + "observation": { + "sender": ["a568af21f540"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "eee847a9d90d" + }, + "state": "7659b8b575da", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.result-absent:settled", + "observation": { + "sender": ["5c71a0c5523d"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "79a49cf80ecf" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.result-null:settled", + "observation": { + "sender": ["af09828e5413"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "79a49cf80ecf" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.inner-ok-missing:settled", + "observation": { + "sender": ["56177906925a"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "0639efe16769" + }, + "state": "6c3ad19b3a5a", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.inner-false-string-error:settled", + "observation": { + "sender": ["c4730e0ddd60"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "0639efe16769" + }, + "state": "6c3ad19b3a5a", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.inner-false-object-error:settled", + "observation": { + "sender": ["a51f35ec407c"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "0639efe16769" + }, + "state": "6c3ad19b3a5a", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.outer-refused:settled", + "observation": { + "sender": ["211cdb1558da"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.outer-refused-no-message:settled", + "observation": { + "sender": ["54071f66c6d8"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.method-not-found:settled", + "observation": { + "sender": ["67dbd4c82715"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.transport-rejection:settled", + "observation": { + "sender": ["6af2188434b2"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "a947768bc0ed" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-image-read.transport-rejection-no-message:settled", + "observation": { + "sender": ["ee977c2d3628"], + "payloads": ["70f782fa6ef2"], + "settlements": { + "load": "c7584e82c72f" + }, + "state": "645c5754be42", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-worktree-text-files.read-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-worktree-text-files.read-1.json new file mode 100644 index 00000000000..d395395e190 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-worktree-text-files.read-1.json @@ -0,0 +1,618 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-worktree-text", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "08d73805619f00748caa5726a7fb0d9b9c561f93ec1e6dd28b27065a4515d65d", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "072647254b82": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "10dde7cd8f0e": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "15467bba2d60": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "1cf4496bc8c0": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 8, + "content": "# readme", + "truncated": false + } + } + } + }, + "3f8bf3069e3d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 8, + "content": "# readme", + "kind": "markdown", + "status": "ready", + "truncated": false + } + }, + "43e3786742e9": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "47ef2e397e18": { + "preview": { + "byteLength": 8, + "content": "# readme", + "kind": "markdown", + "status": "ready", + "truncated": false + } + }, + "645c5754be42": { + "preview": "unloaded" + }, + "786a079aa332": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "8423ff93fda2": { + "name": "files.read#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + }, + "91f40f652017": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9def31fe4536": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "bbbd11388ff3": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "d7d97e5dac06": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "e05b0a48111b": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "fba5e3c89244": { + "preview": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "fcbcdb353528": { + "name": "files.read#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "fed544a8befe": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.read)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + } + }, + "recording": { + "scenario": "matrix-files.preview-worktree-text-files.read-1", + "checkpoints": [ + { + "id": "files-preview-worktree-text-read.normal:settled", + "observation": { + "sender": ["1cf4496bc8c0"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "3f8bf3069e3d" + }, + "state": "47ef2e397e18", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.result-absent:settled", + "observation": { + "sender": ["43e3786742e9"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "fed544a8befe" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.result-null:settled", + "observation": { + "sender": ["10dde7cd8f0e"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "fed544a8befe" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.inner-ok-missing:settled", + "observation": { + "sender": ["fcbcdb353528"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "fed544a8befe" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.inner-false-string-error:settled", + "observation": { + "sender": ["d7d97e5dac06"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "fed544a8befe" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.inner-false-object-error:settled", + "observation": { + "sender": ["072647254b82"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "fed544a8befe" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.outer-refused:settled", + "observation": { + "sender": ["91f40f652017"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.outer-refused-no-message:settled", + "observation": { + "sender": ["bbbd11388ff3"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.method-not-found:settled", + "observation": { + "sender": ["e05b0a48111b"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.transport-rejection:settled", + "observation": { + "sender": ["9def31fe4536"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "a947768bc0ed" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-worktree-text-read.transport-rejection-no-message:settled", + "observation": { + "sender": ["786a079aa332"], + "payloads": ["8423ff93fda2"], + "settlements": { + "load": "c7584e82c72f" + }, + "state": "645c5754be42", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json index b255b694122..24700019285 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json @@ -3,7 +3,7 @@ "family": "files.tab-doc", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -232,24 +232,6 @@ } } }, - "5a33eeedb90f": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "byteLength": { - "$rpc": "undefined" - }, - "content": { - "$rpc": "undefined" - }, - "kind": "file", - "status": "ready", - "truncated": { - "$rpc": "undefined" - } - } - }, "786a079aa332": { "name": "files.read#1", "ordinal": 1, @@ -394,16 +376,6 @@ } } }, - "a7c7f43265d5": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of null (reading 'content')", - "isRpcDeliveryUnknown": false - } - }, "a947768bc0ed": { "status": "rejected", "startedAt": 0, @@ -436,16 +408,6 @@ "isRpcDeliveryUnknown": false } }, - "ba9332ae7bb1": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of undefined (reading 'content')", - "isRpcDeliveryUnknown": false - } - }, "bbbd11388ff3": { "name": "files.read#1", "ordinal": 1, @@ -620,43 +582,6 @@ "isRpcDeliveryUnknown": false } }, - "fb58498a8798": { - "diff": { - "kind": "diff", - "lines": [ - { - "kind": "delete", - "oldLineNumber": 1, - "text": "a" - }, - { - "kind": "add", - "newLineNumber": 1, - "text": "b" - } - ], - "status": "ready", - "truncated": false - }, - "image": { - "dataUri": "data:image/png;base64,aGk=", - "kind": "image", - "status": "ready" - }, - "text": { - "byteLength": { - "$rpc": "undefined" - }, - "content": { - "$rpc": "undefined" - }, - "kind": "file", - "status": "ready", - "truncated": { - "$rpc": "undefined" - } - } - }, "fcbcdb353528": { "name": "files.read#1", "ordinal": 1, @@ -692,6 +617,17 @@ } } }, + "fed544a8befe": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.read)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "ffe1c534d459": { "status": "fulfilled", "startedAt": 0, @@ -738,7 +674,7 @@ "sender": ["43e3786742e9", "96b863d005b1", "096b0c48dd10"], "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { - "text": "ba9332ae7bb1", + "text": "fed544a8befe", "image": "eee847a9d90d", "diff": "ffe1c534d459" }, @@ -752,7 +688,7 @@ "sender": ["10dde7cd8f0e", "96b863d005b1", "096b0c48dd10"], "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { - "text": "a7c7f43265d5", + "text": "fed544a8befe", "image": "eee847a9d90d", "diff": "ffe1c534d459" }, @@ -766,11 +702,11 @@ "sender": ["fcbcdb353528", "96b863d005b1", "096b0c48dd10"], "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { - "text": "5a33eeedb90f", + "text": "fed544a8befe", "image": "eee847a9d90d", "diff": "ffe1c534d459" }, - "state": "fb58498a8798", + "state": "3073ceba86bd", "effects": [] } }, @@ -780,11 +716,11 @@ "sender": ["d7d97e5dac06", "96b863d005b1", "096b0c48dd10"], "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { - "text": "5a33eeedb90f", + "text": "fed544a8befe", "image": "eee847a9d90d", "diff": "ffe1c534d459" }, - "state": "fb58498a8798", + "state": "3073ceba86bd", "effects": [] } }, @@ -794,11 +730,11 @@ "sender": ["072647254b82", "96b863d005b1", "096b0c48dd10"], "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { - "text": "5a33eeedb90f", + "text": "fed544a8befe", "image": "eee847a9d90d", "diff": "ffe1c534d459" }, - "state": "fb58498a8798", + "state": "3073ceba86bd", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json index e125a9ab5ed..a9522036b8f 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json @@ -3,7 +3,7 @@ "family": "files.tab-doc", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -93,16 +93,6 @@ "ordinal": 4, "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.readPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/logo.png\"}}" }, - "2e3bb1c16607": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of undefined (reading 'isImage')", - "isRpcDeliveryUnknown": false - } - }, "32a7c0ae7918": { "status": "rejected", "startedAt": 0, @@ -279,6 +269,17 @@ } } }, + "79a49cf80ecf": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (files.readPreview)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "8423ff93fda2": { "name": "files.read#1", "ordinal": 2, @@ -453,16 +454,6 @@ } } }, - "c38abcaf69dd": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "Error", - "message": "binary_file", - "isRpcDeliveryUnknown": false - } - }, "c7584e82c72f": { "status": "rejected", "startedAt": 0, @@ -514,16 +505,6 @@ } } }, - "d6234620430f": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of null (reading 'isImage')", - "isRpcDeliveryUnknown": false - } - }, "ddb0f42d762d": { "name": "files.readPreview#1", "ordinal": 3, @@ -696,7 +677,7 @@ "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { "text": "b5c68b76c498", - "image": "2e3bb1c16607", + "image": "79a49cf80ecf", "diff": "ffe1c534d459" }, "state": "5521ad94c331", @@ -710,7 +691,7 @@ "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { "text": "b5c68b76c498", - "image": "d6234620430f", + "image": "79a49cf80ecf", "diff": "ffe1c534d459" }, "state": "5521ad94c331", @@ -724,7 +705,7 @@ "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { "text": "b5c68b76c498", - "image": "c38abcaf69dd", + "image": "79a49cf80ecf", "diff": "ffe1c534d459" }, "state": "5521ad94c331", @@ -738,7 +719,7 @@ "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { "text": "b5c68b76c498", - "image": "c38abcaf69dd", + "image": "79a49cf80ecf", "diff": "ffe1c534d459" }, "state": "5521ad94c331", @@ -752,7 +733,7 @@ "payloads": ["8423ff93fda2", "1f1a0d8f6723", "ca069263339b"], "settlements": { "text": "b5c68b76c498", - "image": "c38abcaf69dd", + "image": "79a49cf80ecf", "diff": "ffe1c534d459" }, "state": "5521ad94c331", diff --git a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json index d4034bf5710..40a3d75279d 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json @@ -3,7 +3,7 @@ "family": "files.tab-doc", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", @@ -177,26 +177,6 @@ "isRpcDeliveryUnknown": false } }, - "3a9e5c87d18b": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of undefined (reading 'kind')", - "isRpcDeliveryUnknown": false - } - }, - "559c313a79f9": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of null (reading 'kind')", - "isRpcDeliveryUnknown": false - } - }, "6b721f327587": { "name": "git.diff#1", "ordinal": 5, @@ -235,6 +215,17 @@ "ordinal": 2, "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" }, + "8692c1e96521": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (git.diff)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "96b863d005b1": { "name": "files.readPreview#1", "ordinal": 3, @@ -377,16 +368,6 @@ "isRpcDeliveryUnknown": false } }, - "c38abcaf69dd": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "Error", - "message": "binary_file", - "isRpcDeliveryUnknown": false - } - }, "c7584e82c72f": { "status": "rejected", "startedAt": 0, @@ -695,7 +676,7 @@ "settlements": { "text": "b5c68b76c498", "image": "eee847a9d90d", - "diff": "3a9e5c87d18b" + "diff": "8692c1e96521" }, "state": "e3995cc146f1", "effects": [] @@ -709,7 +690,7 @@ "settlements": { "text": "b5c68b76c498", "image": "eee847a9d90d", - "diff": "559c313a79f9" + "diff": "8692c1e96521" }, "state": "e3995cc146f1", "effects": [] @@ -723,7 +704,7 @@ "settlements": { "text": "b5c68b76c498", "image": "eee847a9d90d", - "diff": "c38abcaf69dd" + "diff": "8692c1e96521" }, "state": "e3995cc146f1", "effects": [] @@ -737,7 +718,7 @@ "settlements": { "text": "b5c68b76c498", "image": "eee847a9d90d", - "diff": "c38abcaf69dd" + "diff": "8692c1e96521" }, "state": "e3995cc146f1", "effects": [] @@ -751,7 +732,7 @@ "settlements": { "text": "b5c68b76c498", "image": "eee847a9d90d", - "diff": "c38abcaf69dd" + "diff": "8692c1e96521" }, "state": "e3995cc146f1", "effects": [] diff --git a/mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.open-1.json b/mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.open-1.json index c67e146f379..2d307403d23 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.open-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.open-1.json @@ -3,7 +3,7 @@ "family": "files.terminal-path-tap", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e20a76ecd5e820dc4797fb25307810af68b5ced996dba2b9599464a21b5cbe1b", diff --git a/mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.resolveterminalpath-1.json b/mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.resolveterminalpath-1.json index 81e5a24e0e5..58906eb7ce8 100644 --- a/mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.resolveterminalpath-1.json +++ b/mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.resolveterminalpath-1.json @@ -3,7 +3,7 @@ "family": "files.terminal-path-tap", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e20a76ecd5e820dc4797fb25307810af68b5ced996dba2b9599464a21b5cbe1b", diff --git a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json index 03b994934f0..2680929c395 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json @@ -3,7 +3,7 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json index 8547f875930..23eaf889f6a 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json @@ -3,7 +3,7 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json index 25646a38e7b..f21fba36b6a 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json @@ -3,7 +3,7 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/matrix-git.branch-diff-preview-git.branchdiff-1.json b/mobile/rpc-foundation/goldens/matrix-git.branch-diff-preview-git.branchdiff-1.json index 977e1b80e49..811eeaad579 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.branch-diff-preview-git.branchdiff-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.branch-diff-preview-git.branchdiff-1.json @@ -3,7 +3,7 @@ "family": "git.branch-diff-preview", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json b/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json index 9c9519dd5d4..91ac30b8839 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json @@ -3,7 +3,7 @@ "family": "git.changes-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json b/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json index 62ad930c670..57d24564b55 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json @@ -3,7 +3,7 @@ "family": "git.changes-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json index 17ccd951f1b..f3f06b5023b 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json @@ -3,7 +3,7 @@ "family": "git.changes-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json index 9b36da86669..d520d2e5e56 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json @@ -3,7 +3,7 @@ "family": "git.changes-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json b/mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json index 88ba8451bcc..76cb7144be5 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json @@ -3,7 +3,7 @@ "family": "git.commit-message-ai", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.commitcompare-1.json b/mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.commitcompare-1.json index c88a57bc9ac..817329b6104 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.commitcompare-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.commitcompare-1.json @@ -3,7 +3,7 @@ "family": "git.history-commit-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.history-1.json b/mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.history-1.json index c5ef406878f..cce23046c6f 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.history-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.history-1.json @@ -3,7 +3,7 @@ "family": "git.history-commit-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json b/mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json index 2c3bc4479d5..e66e92c4f18 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json @@ -3,7 +3,7 @@ "family": "git.history-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json b/mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json index 54d24163824..a5e3b14cb90 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json @@ -3,7 +3,7 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json b/mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json index 8c3833a01aa..8ad4a5faf20 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json @@ -3,7 +3,7 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addissuecomment-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addissuecomment-1.json index 61fddd15d64..1455515e450 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addissuecomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addissuecomment-1.json @@ -3,7 +3,7 @@ "family": "github.pr-comment-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addprreviewcommentreply-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addprreviewcommentreply-1.json index 1b41fb135c0..f43797fe303 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addprreviewcommentreply-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addprreviewcommentreply-1.json @@ -3,7 +3,7 @@ "family": "github.pr-comment-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.deleteissuecommentbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.deleteissuecommentbyslug-1.json index 72eed2e4103..53786d16204 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.deleteissuecommentbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.deleteissuecommentbyslug-1.json @@ -3,7 +3,7 @@ "family": "github.pr-comment-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.updateissuecommentbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.updateissuecommentbyslug-1.json index 147c3265b75..b59a0b84edc 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.updateissuecommentbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.updateissuecommentbyslug-1.json @@ -3,7 +3,7 @@ "family": "github.pr-comment-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.resolvereviewthread-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.resolvereviewthread-1.json index 12c1edeab21..8d64298ac3f 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.resolvereviewthread-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.resolvereviewthread-1.json @@ -3,7 +3,7 @@ "family": "github.pr-comment-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.mergepr-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.mergepr-1.json index bd40b27c25c..903bde1e5f0 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.mergepr-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.mergepr-1.json @@ -3,7 +3,7 @@ "family": "github.pr-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.removeprreviewers-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.removeprreviewers-1.json index 30796255b48..bcb49e675ca 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.removeprreviewers-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.removeprreviewers-1.json @@ -3,7 +3,7 @@ "family": "github.pr-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.requestprreviewers-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.requestprreviewers-1.json index 880018bfcd7..a0b3d9b73d4 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.requestprreviewers-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.requestprreviewers-1.json @@ -3,7 +3,7 @@ "family": "github.pr-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.rerunprchecks-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.rerunprchecks-1.json index 835342d4fee..b0b358e1f6a 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.rerunprchecks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.rerunprchecks-1.json @@ -3,7 +3,7 @@ "family": "github.pr-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.setprautomerge-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.setprautomerge-1.json index f33638d28bf..3aa180ea704 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.setprautomerge-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.setprautomerge-1.json @@ -3,7 +3,7 @@ "family": "github.pr-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.updateprstate-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.updateprstate-1.json index 0ba3be45f06..64f54c38e8c 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.updateprstate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.updateprstate-1.json @@ -3,7 +3,7 @@ "family": "github.pr-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.listassignableusers-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.listassignableusers-1.json index 9de6c1c40a6..af67fa26a13 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.listassignableusers-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.listassignableusers-1.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prcheckdetails-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prcheckdetails-1.json index ad34dfdeefb..0a32cba06dd 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prcheckdetails-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prcheckdetails-1.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prchecks-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prchecks-1.json index b2b41509fc6..77af6982c86 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prchecks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prchecks-1.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prforbranch-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prforbranch-1.json index a4c56a7cdef..651579209c4 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prforbranch-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prforbranch-1.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.reposlug-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.reposlug-1.json index b3b8d4d7180..f9c63e81960 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.reposlug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.reposlug-1.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.workitemdetails-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.workitemdetails-1.json index 9d0f50c0523..9fe0850b18e 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.workitemdetails-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-read-github.workitemdetails-1.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-read-hostedreview.forbranch-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-read-hostedreview.forbranch-1.json index cb39134ab65..4d3e118c942 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-read-hostedreview.forbranch-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-read-hostedreview.forbranch-1.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-github.pr-title-mutation-github.updateprtitle-1.json b/mobile/rpc-foundation/goldens/matrix-github.pr-title-mutation-github.updateprtitle-1.json index 61201f95439..1c92dc53ce1 100644 --- a/mobile/rpc-foundation/goldens/matrix-github.pr-title-mutation-github.updateprtitle-1.json +++ b/mobile/rpc-foundation/goldens/matrix-github.pr-title-mutation-github.updateprtitle-1.json @@ -3,7 +3,7 @@ "family": "github.pr-title-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-home.host-accounts-accounts.list-1.json b/mobile/rpc-foundation/goldens/matrix-home.host-accounts-accounts.list-1.json index e4961b2719b..e63b82c445b 100644 --- a/mobile/rpc-foundation/goldens/matrix-home.host-accounts-accounts.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-home.host-accounts-accounts.list-1.json @@ -3,7 +3,7 @@ "family": "home.host-accounts", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c632fdbc4b730777ecb09f08bec14cca0586042b01ed99d40d0228806c7def4a", diff --git a/mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json b/mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json index a6e515a6389..7f1b441ac75 100644 --- a/mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json +++ b/mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json @@ -3,7 +3,7 @@ "family": "home.host-stats", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", diff --git a/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-1.json b/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-1.json index 6fcc70596b4..5a0257c7801 100644 --- a/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-1.json +++ b/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-1.json @@ -3,7 +3,7 @@ "family": "host-worktree-refresh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-2.json b/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-2.json index 4ff98187ada..733e07b77e7 100644 --- a/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-2.json +++ b/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-2.json @@ -3,7 +3,7 @@ "family": "host-worktree-refresh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-3.json b/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-3.json index 69d091a5731..68892d46f4f 100644 --- a/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-3.json +++ b/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-3.json @@ -3,7 +3,7 @@ "family": "host-worktree-refresh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-2-1.json b/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-2-1.json index b7982cd1963..f18b0bdc542 100644 --- a/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-2-1.json +++ b/mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-2-1.json @@ -3,7 +3,7 @@ "family": "host-worktree-refresh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json b/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json index e207bddfe97..59e8bfce095 100644 --- a/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json @@ -3,7 +3,7 @@ "family": "host.view-settings", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", diff --git a/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json b/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json index e3c3f78bc30..ffd745529ec 100644 --- a/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json @@ -3,7 +3,7 @@ "family": "host.view-settings", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", diff --git a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json index 97aef5cc561..c94c38a4c3e 100644 --- a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json @@ -3,7 +3,7 @@ "family": "host.worktree-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", diff --git a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json index bf618e0c183..69225d9a8f7 100644 --- a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json +++ b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json @@ -3,7 +3,7 @@ "family": "host.worktree-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", diff --git a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json index 36f03243e4d..584b62fcd3e 100644 --- a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json @@ -3,7 +3,7 @@ "family": "host.worktree-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json index 040661657bb..c37161c6c30 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json index b99ce9c31a5..dab55c61515 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json index c749e7d5409..205224357f5 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json index 81b2109d230..8a2fa23cd82 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json index 78749d8d575..22df417024d 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json index e2098e6afa4..7db76a5d39d 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json index 1cf8d4eb769..390618e1fc1 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json index f0a261c8196..0777ac8a6d9 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json index f15cfaa35cd..bb03a2497a8 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json index d823fb16896..adc516270d3 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json index 0a4020a7061..3f140726e98 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json index 6f46787861c..ae23a6231d4 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json index 5827fabba52..dd74ad07fbd 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json index e301c17084d..a6082edeac0 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json index 18f4447597d..51f082676e2 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json index f736250084f..daa10145bb7 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json @@ -3,7 +3,7 @@ "family": "hostedReview.eligibility", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json index b61dc48bf6c..25406cd2d31 100644 --- a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json +++ b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json index 36994bfc203..57c38e3b259 100644 --- a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json +++ b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json index 0cf82b9b9f6..869ddb226d0 100644 --- a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json +++ b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json index 3ef9f1123a1..78ac874bc0c 100644 --- a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json +++ b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json @@ -3,7 +3,7 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json b/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json index 9d74f74b064..421ff6284b9 100644 --- a/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json @@ -3,7 +3,7 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json b/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json index 83c1591445b..5b9a290d256 100644 --- a/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json +++ b/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json @@ -3,7 +3,7 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/matrix-linear.select-workspace-picker-linear.selectworkspace-1.json b/mobile/rpc-foundation/goldens/matrix-linear.select-workspace-picker-linear.selectworkspace-1.json index 3f2e988bdce..ffe62097acf 100644 --- a/mobile/rpc-foundation/goldens/matrix-linear.select-workspace-picker-linear.selectworkspace-1.json +++ b/mobile/rpc-foundation/goldens/matrix-linear.select-workspace-picker-linear.selectworkspace-1.json @@ -3,7 +3,7 @@ "family": "linear.select-workspace-picker", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b65996c152b632d553a31e07e31ea5c76f998eada42cf0966923d730da21908f", diff --git a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-1.json b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-1.json index 5d9d129ac36..8941f661495 100644 --- a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-1.json +++ b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-1.json @@ -3,7 +3,7 @@ "family": "live-worktree-name", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-2.json b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-2.json index 5b938b195f9..d4224570656 100644 --- a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-2.json +++ b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-2.json @@ -3,7 +3,7 @@ "family": "live-worktree-name", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-2-1.json b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-2-1.json index 3af7a602561..0b24fca0839 100644 --- a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-2-1.json +++ b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-2-1.json @@ -3,7 +3,7 @@ "family": "live-worktree-name", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-1.json index 9b22e11e6eb..d48f69b5a83 100644 --- a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-1.json @@ -3,7 +3,7 @@ "family": "live-worktree-name", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-2.json b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-2.json index 7552e195797..b5bbc67ce27 100644 --- a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-2.json +++ b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-2.json @@ -3,7 +3,7 @@ "family": "live-worktree-name", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-3.json b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-3.json index 431e5026b98..c3568a8fdc1 100644 --- a/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-3.json +++ b/mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-3.json @@ -3,7 +3,7 @@ "family": "live-worktree-name", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-app-js.json b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-app-js.json new file mode 100644 index 00000000000..0f28580663d --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-app-js.json @@ -0,0 +1,853 @@ +{ + "operation": "mobileWeb.bundle-fetch", + "family": "mobileWeb.bundle-fetch", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "3fa9e96357f3df313cca9632bcbc79e0c41f14b055e89dd4b848d8c27ed5ff4d", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0675365edeab": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "11447b4712c1": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: " + }, + "19aa61aab0e5": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: refused: " + }, + "23871e324a00": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + } + }, + "2988ade7daff": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 6, + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":0}}" + }, + "2bfa7c81f55f": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 9, + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":16}}" + }, + "2ff2addcb1f6": { + "name": "bundle-progress", + "ordinal": 10, + "value": { + "completedAssets": 2, + "receivedBytes": 37, + "totalAssets": 2 + } + }, + "467f0030b66f": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: The host sent a reply this app could not read (mobileWeb.bundle.chunk)" + }, + "4e3b57d795cb": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: outer refused", + "isRpcDeliveryUnknown": false + } + }, + "573943fdb37d": { + "assets": { + "assets/app.js": "orca.boot()", + "index.html": "

orca

" + }, + "outcome": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "5f7979d761c6": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "6819e6630a90": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "70ad183ccf84": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: refused: outer refused" + }, + "714b5d0de2ed": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "76ca82e57d04": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "PCFkb2N0eXBlIGh0bWw+PA==", + "eof": false, + "offset": 0, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "782d48c615ad": { + "name": "bundle-progress", + "ordinal": 7, + "value": { + "completedAssets": 1, + "receivedBytes": 11, + "totalAssets": 2 + } + }, + "84a5b50c6206": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: method_not_found: Unknown method" + }, + "a0c050b31ee2": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"assets/app.js\",\"offset\":0}}" + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ae7f1100e6e5": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "assetByteLength": 11, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "b3JjYS5ib290KCk=", + "eof": true, + "offset": 0, + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + } + } + } + }, + "b722dc19c21f": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "b9f5f0928755": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: transport failure" + }, + "bf3d420631ad": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "c2390891036b": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "c319ae866f13": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c8c78c1816d5": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (mobileWeb.bundle.chunk)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, + "ce7abd9f93bb": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "cD5vcmNhPC9wPg==", + "eof": true, + "offset": 16, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "d56bfdbce702": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: ", + "isRpcDeliveryUnknown": false + } + }, + "ec2ac0525db0": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "f33db2b3742d": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "f624ac81d963": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "method_not_found: Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "f7c0f62c1992": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + } + }, + "recording": { + "scenario": "matrix-mobileweb.bundle-fetch-app-js", + "checkpoints": [ + { + "id": "mobile-web-bundle-fetch-paged.normal:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "ce7abd9f93bb"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c319ae866f13" + }, + "state": "573943fdb37d", + "effects": ["782d48c615ad", "2ff2addcb1f6"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.result-absent:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "6819e6630a90", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.result-null:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "bf3d420631ad", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-ok-missing:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "5f7979d761c6", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-false-string-error:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "b722dc19c21f", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-false-object-error:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "f7c0f62c1992", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.outer-refused:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "c2390891036b", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "4e3b57d795cb" + }, + "state": "70ad183ccf84", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.outer-refused-no-message:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "714b5d0de2ed", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "d56bfdbce702" + }, + "state": "19aa61aab0e5", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.method-not-found:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "0675365edeab", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "f624ac81d963" + }, + "state": "84a5b50c6206", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.transport-rejection:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ec2ac0525db0", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "a947768bc0ed" + }, + "state": "b9f5f0928755", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.transport-rejection-no-message:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "f33db2b3742d", "76ca82e57d04"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c7584e82c72f" + }, + "state": "11447b4712c1", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-head.json b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-head.json new file mode 100644 index 00000000000..b92312906ea --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-head.json @@ -0,0 +1,853 @@ +{ + "operation": "mobileWeb.bundle-fetch", + "family": "mobileWeb.bundle-fetch", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "73734d83e9bc272cba83255b64a21ccab62561d4b6c753b8e2bbb07d6d7b9cc4", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "11447b4712c1": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: " + }, + "19aa61aab0e5": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: refused: " + }, + "1c53908ac349": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "2142bf483ffa": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-3", + "ok": false + } + } + }, + "23871e324a00": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + } + }, + "2988ade7daff": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 6, + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":0}}" + }, + "2bfa7c81f55f": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 9, + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":16}}" + }, + "2c0da0506922": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "2ff2addcb1f6": { + "name": "bundle-progress", + "ordinal": 10, + "value": { + "completedAssets": 2, + "receivedBytes": 37, + "totalAssets": 2 + } + }, + "44b044c3bcbb": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-3", + "ok": false + } + } + }, + "467f0030b66f": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: The host sent a reply this app could not read (mobileWeb.bundle.chunk)" + }, + "48a06fd06c9f": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-3", + "ok": false + } + } + }, + "4e3b57d795cb": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: outer refused", + "isRpcDeliveryUnknown": false + } + }, + "51aab07cd069": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "573943fdb37d": { + "assets": { + "assets/app.js": "orca.boot()", + "index.html": "

orca

" + }, + "outcome": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "5f506b45b4da": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "70ad183ccf84": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: refused: outer refused" + }, + "76ca82e57d04": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "PCFkb2N0eXBlIGh0bWw+PA==", + "eof": false, + "offset": 0, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "782d48c615ad": { + "name": "bundle-progress", + "ordinal": 7, + "value": { + "completedAssets": 1, + "receivedBytes": 11, + "totalAssets": 2 + } + }, + "78a565992eac": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "84a5b50c6206": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: method_not_found: Unknown method" + }, + "a0c050b31ee2": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"assets/app.js\",\"offset\":0}}" + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ae7f1100e6e5": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "assetByteLength": 11, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "b3JjYS5ib290KCk=", + "eof": true, + "offset": 0, + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + } + } + } + }, + "b93a57c52fa0": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true + } + } + }, + "b9f5f0928755": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: transport failure" + }, + "c319ae866f13": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c8c78c1816d5": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (mobileWeb.bundle.chunk)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, + "ca40206d195c": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "ce7abd9f93bb": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "cD5vcmNhPC9wPg==", + "eof": true, + "offset": 16, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "d56bfdbce702": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: ", + "isRpcDeliveryUnknown": false + } + }, + "f624ac81d963": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "method_not_found: Unknown method", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "matrix-mobileweb.bundle-fetch-index-head", + "checkpoints": [ + { + "id": "mobile-web-bundle-fetch-paged.normal:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "ce7abd9f93bb"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c319ae866f13" + }, + "state": "573943fdb37d", + "effects": ["782d48c615ad", "2ff2addcb1f6"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.result-absent:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "b93a57c52fa0"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.result-null:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "ca40206d195c"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-ok-missing:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "1c53908ac349"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-false-string-error:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "5f506b45b4da"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-false-object-error:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "2c0da0506922"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.outer-refused:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "48a06fd06c9f"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "4e3b57d795cb" + }, + "state": "70ad183ccf84", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.outer-refused-no-message:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "44b044c3bcbb"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "d56bfdbce702" + }, + "state": "19aa61aab0e5", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.method-not-found:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "2142bf483ffa"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "f624ac81d963" + }, + "state": "84a5b50c6206", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.transport-rejection:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "78a565992eac"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "a947768bc0ed" + }, + "state": "b9f5f0928755", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.transport-rejection-no-message:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "51aab07cd069"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "c7584e82c72f" + }, + "state": "11447b4712c1", + "effects": ["782d48c615ad"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-tail.json b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-tail.json new file mode 100644 index 00000000000..c323cf9fd41 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-tail.json @@ -0,0 +1,853 @@ +{ + "operation": "mobileWeb.bundle-fetch", + "family": "mobileWeb.bundle-fetch", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "dd6d7a35c8ce229211f693ce7b581daecd9f1d7923efb09aff190ea6c46a8b66", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0e77845a234c": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "0fd8cd28042f": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "11447b4712c1": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: " + }, + "19aa61aab0e5": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: refused: " + }, + "23871e324a00": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + } + }, + "2988ade7daff": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 6, + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":0}}" + }, + "2bfa7c81f55f": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 9, + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":16}}" + }, + "2ff2addcb1f6": { + "name": "bundle-progress", + "ordinal": 10, + "value": { + "completedAssets": 2, + "receivedBytes": 37, + "totalAssets": 2 + } + }, + "31b995bd644d": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "467f0030b66f": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: The host sent a reply this app could not read (mobileWeb.bundle.chunk)" + }, + "4e3b57d795cb": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: outer refused", + "isRpcDeliveryUnknown": false + } + }, + "573943fdb37d": { + "assets": { + "assets/app.js": "orca.boot()", + "index.html": "

orca

" + }, + "outcome": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "582980d0a726": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "6a71630ead47": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "70ab70e5f72c": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "70ad183ccf84": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: refused: outer refused" + }, + "76ca82e57d04": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "PCFkb2N0eXBlIGh0bWw+PA==", + "eof": false, + "offset": 0, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "77da9e3f0b42": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true + } + } + }, + "782d48c615ad": { + "name": "bundle-progress", + "ordinal": 7, + "value": { + "completedAssets": 1, + "receivedBytes": 11, + "totalAssets": 2 + } + }, + "84a5b50c6206": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: method_not_found: Unknown method" + }, + "a0c050b31ee2": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"assets/app.js\",\"offset\":0}}" + }, + "a1a88d7a4c18": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-4", + "ok": false + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ae7f1100e6e5": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "assetByteLength": 11, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "b3JjYS5ib290KCk=", + "eof": true, + "offset": 0, + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + } + } + } + }, + "b9f5f0928755": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: transport failure" + }, + "c319ae866f13": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c8c78c1816d5": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (mobileWeb.bundle.chunk)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, + "ce7abd9f93bb": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "cD5vcmNhPC9wPg==", + "eof": true, + "offset": 16, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "d1c2396755f2": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-4", + "ok": false + } + } + }, + "d3f6932f076d": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-4", + "ok": false + } + } + }, + "d56bfdbce702": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: ", + "isRpcDeliveryUnknown": false + } + }, + "f624ac81d963": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "method_not_found: Unknown method", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "matrix-mobileweb.bundle-fetch-index-tail", + "checkpoints": [ + { + "id": "mobile-web-bundle-fetch-paged.normal:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "ce7abd9f93bb"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c319ae866f13" + }, + "state": "573943fdb37d", + "effects": ["782d48c615ad", "2ff2addcb1f6"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.result-absent:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "77da9e3f0b42"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.result-null:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "6a71630ead47"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-ok-missing:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "31b995bd644d"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-false-string-error:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "70ab70e5f72c"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-false-object-error:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "0e77845a234c"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c8c78c1816d5" + }, + "state": "467f0030b66f", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.outer-refused:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "d1c2396755f2"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "4e3b57d795cb" + }, + "state": "70ad183ccf84", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.outer-refused-no-message:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "d3f6932f076d"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "d56bfdbce702" + }, + "state": "19aa61aab0e5", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.method-not-found:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "a1a88d7a4c18"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "f624ac81d963" + }, + "state": "84a5b50c6206", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.transport-rejection:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "582980d0a726"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "a947768bc0ed" + }, + "state": "b9f5f0928755", + "effects": ["782d48c615ad"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.transport-rejection-no-message:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "0fd8cd28042f"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c7584e82c72f" + }, + "state": "11447b4712c1", + "effects": ["782d48c615ad"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-mobileweb.bundle.manifest-1.json b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-mobileweb.bundle.manifest-1.json new file mode 100644 index 00000000000..4aa6817740e --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-mobileweb.bundle.manifest-1.json @@ -0,0 +1,833 @@ +{ + "operation": "mobileWeb.bundle-fetch", + "family": "mobileWeb.bundle-fetch", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "d8e4c64949caef9f98ec67aad62a482d0ecbc4bcc611ba5e4d2e73764bf80ea3", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "04c76c033ffb": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: The host sent a reply this app could not read (mobileWeb.bundle.manifest)" + }, + "11447b4712c1": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: " + }, + "19aa61aab0e5": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: refused: " + }, + "1f83ee5bcef4": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "23871e324a00": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + } + }, + "2988ade7daff": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 6, + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":0}}" + }, + "2bfa7c81f55f": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 9, + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":16}}" + }, + "2f77c40dc9fb": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "2ff2addcb1f6": { + "name": "bundle-progress", + "ordinal": 10, + "value": { + "completedAssets": 2, + "receivedBytes": 37, + "totalAssets": 2 + } + }, + "4e3b57d795cb": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: outer refused", + "isRpcDeliveryUnknown": false + } + }, + "573943fdb37d": { + "assets": { + "assets/app.js": "orca.boot()", + "index.html": "

orca

" + }, + "outcome": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "70ad183ccf84": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: refused: outer refused" + }, + "76ca82e57d04": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "PCFkb2N0eXBlIGh0bWw+PA==", + "eof": false, + "offset": 0, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "782d48c615ad": { + "name": "bundle-progress", + "ordinal": 7, + "value": { + "completedAssets": 1, + "receivedBytes": 11, + "totalAssets": 2 + } + }, + "849b557190c4": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (mobileWeb.bundle.manifest)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, + "84a5b50c6206": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: method_not_found: Unknown method" + }, + "92180fc007fa": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "959e5b0d7569": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9bd7a475c5b3": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9f8f097c4318": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "a0c050b31ee2": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"assets/app.js\",\"offset\":0}}" + }, + "a0fb89567920": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ad5c054356d4": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "ae7f1100e6e5": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "assetByteLength": 11, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "b3JjYS5ib290KCk=", + "eof": true, + "offset": 0, + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + } + } + } + }, + "b539b28b8552": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "b9f5f0928755": { + "assets": { + "$rpc": "null" + }, + "outcome": "failed: transport failure" + }, + "c319ae866f13": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "ce7abd9f93bb": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "cD5vcmNhPC9wPg==", + "eof": true, + "offset": 16, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "d54e9333d433": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "d56bfdbce702": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: ", + "isRpcDeliveryUnknown": false + } + }, + "f624ac81d963": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "method_not_found: Unknown method", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "matrix-mobileweb.bundle-fetch-mobileweb.bundle.manifest-1", + "checkpoints": [ + { + "id": "mobile-web-bundle-fetch-paged.normal:bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "ce7abd9f93bb"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c319ae866f13" + }, + "state": "573943fdb37d", + "effects": ["782d48c615ad", "2ff2addcb1f6"] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.result-absent:bundle-fetched", + "observation": { + "sender": ["d54e9333d433"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "849b557190c4" + }, + "state": "04c76c033ffb", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.result-null:bundle-fetched", + "observation": { + "sender": ["1f83ee5bcef4"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "849b557190c4" + }, + "state": "04c76c033ffb", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-ok-missing:bundle-fetched", + "observation": { + "sender": ["b539b28b8552"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "849b557190c4" + }, + "state": "04c76c033ffb", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-false-string-error:bundle-fetched", + "observation": { + "sender": ["ad5c054356d4"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "849b557190c4" + }, + "state": "04c76c033ffb", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.inner-false-object-error:bundle-fetched", + "observation": { + "sender": ["2f77c40dc9fb"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "849b557190c4" + }, + "state": "04c76c033ffb", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.outer-refused:bundle-fetched", + "observation": { + "sender": ["9f8f097c4318"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "4e3b57d795cb" + }, + "state": "70ad183ccf84", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.outer-refused-no-message:bundle-fetched", + "observation": { + "sender": ["959e5b0d7569"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "d56bfdbce702" + }, + "state": "19aa61aab0e5", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.method-not-found:bundle-fetched", + "observation": { + "sender": ["9bd7a475c5b3"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "f624ac81d963" + }, + "state": "84a5b50c6206", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.transport-rejection:bundle-fetched", + "observation": { + "sender": ["a0fb89567920"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "a947768bc0ed" + }, + "state": "b9f5f0928755", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-fetch-paged.transport-rejection-no-message:bundle-fetched", + "observation": { + "sender": ["92180fc007fa"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "c7584e82c72f" + }, + "state": "11447b4712c1", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-manifest-mobileweb.bundle.manifest-1.json b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-manifest-mobileweb.bundle.manifest-1.json new file mode 100644 index 00000000000..673c6bcde0c --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-manifest-mobileweb.bundle.manifest-1.json @@ -0,0 +1,674 @@ +{ + "operation": "mobileWeb.bundle-manifest", + "family": "mobileWeb.bundle-manifest", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "8f5211fd3de1d77d3ea139f22bd6fc6119c72c08aa016f252a0df14bedf23ab3", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "12e970df3db8": { + "outcome": "failed: refused: outer refused" + }, + "1f83ee5bcef4": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "23871e324a00": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + } + }, + "2f77c40dc9fb": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "37fa2b81fff0": { + "outcome": "failed: method_not_found: Unknown method" + }, + "4e3b57d795cb": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: outer refused", + "isRpcDeliveryUnknown": false + } + }, + "533d41ae6475": { + "outcome": "failed: " + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "849b557190c4": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (mobileWeb.bundle.manifest)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, + "92180fc007fa": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "959e5b0d7569": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9bd7a475c5b3": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9ca9b2d0fc07": { + "outcome": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "chunkBytes": 16, + "entrypoint": "index.html", + "paths": ["assets/app.js", "index.html"] + } + }, + "9d87b3b6bd74": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + }, + "9f8f097c4318": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "a0fb89567920": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ad5c054356d4": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "b539b28b8552": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "ce45a7dbe0d4": { + "outcome": "failed: refused: " + }, + "ce8ef76590e2": { + "outcome": "failed: The host sent a reply this app could not read (mobileWeb.bundle.manifest)" + }, + "d54e9333d433": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "d56bfdbce702": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused: ", + "isRpcDeliveryUnknown": false + } + }, + "ded62b26901e": { + "outcome": "failed: transport failure" + }, + "f624ac81d963": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "method_not_found: Unknown method", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "matrix-mobileweb.bundle-manifest-mobileweb.bundle.manifest-1", + "checkpoints": [ + { + "id": "mobile-web-bundle-manifest-read.normal:manifest-read", + "observation": { + "sender": ["23871e324a00"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "9d87b3b6bd74" + }, + "state": "9ca9b2d0fc07", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.result-absent:manifest-read", + "observation": { + "sender": ["d54e9333d433"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "849b557190c4" + }, + "state": "ce8ef76590e2", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.result-null:manifest-read", + "observation": { + "sender": ["1f83ee5bcef4"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "849b557190c4" + }, + "state": "ce8ef76590e2", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.inner-ok-missing:manifest-read", + "observation": { + "sender": ["b539b28b8552"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "849b557190c4" + }, + "state": "ce8ef76590e2", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.inner-false-string-error:manifest-read", + "observation": { + "sender": ["ad5c054356d4"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "849b557190c4" + }, + "state": "ce8ef76590e2", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.inner-false-object-error:manifest-read", + "observation": { + "sender": ["2f77c40dc9fb"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "849b557190c4" + }, + "state": "ce8ef76590e2", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.outer-refused:manifest-read", + "observation": { + "sender": ["9f8f097c4318"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "4e3b57d795cb" + }, + "state": "12e970df3db8", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.outer-refused-no-message:manifest-read", + "observation": { + "sender": ["959e5b0d7569"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "d56bfdbce702" + }, + "state": "ce45a7dbe0d4", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.method-not-found:manifest-read", + "observation": { + "sender": ["9bd7a475c5b3"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "f624ac81d963" + }, + "state": "37fa2b81fff0", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.transport-rejection:manifest-read", + "observation": { + "sender": ["a0fb89567920"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "a947768bc0ed" + }, + "state": "ded62b26901e", + "effects": [] + } + }, + { + "id": "mobile-web-bundle-manifest-read.transport-rejection-no-message:manifest-read", + "observation": { + "sender": ["92180fc007fa"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "c7584e82c72f" + }, + "state": "533d41ae6475", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-1.json index 76d43b423ae..eb9af3347dd 100644 --- a/mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-2.json b/mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-2.json index 56138d86639..3d5af994e58 100644 --- a/mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-2.json +++ b/mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-2.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/matrix-nativechat.image-upload-clipboard.startimageupload-1.json b/mobile/rpc-foundation/goldens/matrix-nativechat.image-upload-clipboard.startimageupload-1.json index 277377aa69a..5a232e4778c 100644 --- a/mobile/rpc-foundation/goldens/matrix-nativechat.image-upload-clipboard.startimageupload-1.json +++ b/mobile/rpc-foundation/goldens/matrix-nativechat.image-upload-clipboard.startimageupload-1.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/matrix-nativechat.session-option-pick-settings.mutatenativechatsessionoptions-1.json b/mobile/rpc-foundation/goldens/matrix-nativechat.session-option-pick-settings.mutatenativechatsessionoptions-1.json index b6728a5853e..ad1b01dc79a 100644 --- a/mobile/rpc-foundation/goldens/matrix-nativechat.session-option-pick-settings.mutatenativechatsessionoptions-1.json +++ b/mobile/rpc-foundation/goldens/matrix-nativechat.session-option-pick-settings.mutatenativechatsessionoptions-1.json @@ -3,7 +3,7 @@ "family": "nativeChat.session-option-pick", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-orchestration.workerterminaluserinput-1.json b/mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-orchestration.workerterminaluserinput-1.json index 40aaabd7a83..658c5fa15eb 100644 --- a/mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-orchestration.workerterminaluserinput-1.json +++ b/mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-orchestration.workerterminaluserinput-1.json @@ -3,7 +3,7 @@ "family": "nativeChat.terminal-write", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-terminal.send-1.json index cd64d510252..1ee62b745f7 100644 --- a/mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "nativeChat.terminal-write", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.getmissedsince-1.json b/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.getmissedsince-1.json index 6323e995659..418fb01baae 100644 --- a/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.getmissedsince-1.json +++ b/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.getmissedsince-1.json @@ -3,7 +3,7 @@ "family": "notifications.desktop-stream", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "eacf859143588ae6bee2804975d642b6c1088d57ae77ffe298620250d9a9f0e4", diff --git a/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-1.json b/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-1.json index 7656a5c6038..25df5c608b5 100644 --- a/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-1.json +++ b/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-1.json @@ -3,7 +3,7 @@ "family": "notifications.desktop-stream", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "eacf859143588ae6bee2804975d642b6c1088d57ae77ffe298620250d9a9f0e4", diff --git a/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-2.json b/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-2.json index 10dfd7730e9..2bfe76d3c6d 100644 --- a/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-2.json +++ b/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-2.json @@ -3,7 +3,7 @@ "family": "notifications.desktop-stream", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "eacf859143588ae6bee2804975d642b6c1088d57ae77ffe298620250d9a9f0e4", diff --git a/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.unsubscribe-1.json b/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.unsubscribe-1.json index 3e45c51d260..9b35fe7aded 100644 --- a/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.unsubscribe-1.json +++ b/mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.unsubscribe-1.json @@ -3,7 +3,7 @@ "family": "notifications.desktop-stream", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "eacf859143588ae6bee2804975d642b6c1088d57ae77ffe298620250d9a9f0e4", diff --git a/mobile/rpc-foundation/goldens/matrix-notifications.display-test-screen-notifications.testpush-1.json b/mobile/rpc-foundation/goldens/matrix-notifications.display-test-screen-notifications.testpush-1.json index de6ea78eb8a..4186fbf5cd4 100644 --- a/mobile/rpc-foundation/goldens/matrix-notifications.display-test-screen-notifications.testpush-1.json +++ b/mobile/rpc-foundation/goldens/matrix-notifications.display-test-screen-notifications.testpush-1.json @@ -3,7 +3,7 @@ "family": "notifications.display-test-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "12a04986290715d5db23a1eb5192c1138bb2d375d1b1dd37ba310ea89eb11566", diff --git a/mobile/rpc-foundation/goldens/matrix-notifications.push-dismissal-notifications.getmissedsince-1.json b/mobile/rpc-foundation/goldens/matrix-notifications.push-dismissal-notifications.getmissedsince-1.json index e80f44c38c0..e35222077a4 100644 --- a/mobile/rpc-foundation/goldens/matrix-notifications.push-dismissal-notifications.getmissedsince-1.json +++ b/mobile/rpc-foundation/goldens/matrix-notifications.push-dismissal-notifications.getmissedsince-1.json @@ -3,7 +3,7 @@ "family": "notifications.push-dismissal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "595a3eb2994d0596b9fcd0707b175b4e978625053dfbc5c541b0350c0cbfb524", diff --git a/mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.registerpush-1.json b/mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.registerpush-1.json index fb068951c77..8a760ce0715 100644 --- a/mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.registerpush-1.json +++ b/mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.registerpush-1.json @@ -3,7 +3,7 @@ "family": "notifications.push-registration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2e3d939dc162dbc5a38d8a7207111688204a825fd70348721917b3016e1c9470", diff --git a/mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.unregisterpush-1.json b/mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.unregisterpush-1.json index 0dc93d80693..0695c4c18f2 100644 --- a/mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.unregisterpush-1.json +++ b/mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.unregisterpush-1.json @@ -3,7 +3,7 @@ "family": "notifications.push-registration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2e3d939dc162dbc5a38d8a7207111688204a825fd70348721917b3016e1c9470", diff --git a/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-direct-status.json b/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-direct-status.json index 21d3ffcde32..e0b892d1427 100644 --- a/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-direct-status.json +++ b/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-direct-status.json @@ -3,7 +3,7 @@ "family": "pairing.pre-profile", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.getendpoints-1.json b/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.getendpoints-1.json index fe2b42f2b01..82852f532c0 100644 --- a/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.getendpoints-1.json +++ b/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.getendpoints-1.json @@ -3,7 +3,7 @@ "family": "pairing.pre-profile", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.provisionrelay-1.json b/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.provisionrelay-1.json index 060481b347a..d1429543e20 100644 --- a/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.provisionrelay-1.json +++ b/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.provisionrelay-1.json @@ -3,7 +3,7 @@ "family": "pairing.pre-profile", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-relay-status.json b/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-relay-status.json index a36cb62ca95..bcbee6b1e5b 100644 --- a/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-relay-status.json +++ b/mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-relay-status.json @@ -3,7 +3,7 @@ "family": "pairing.pre-profile", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json b/mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json index 6d7820eb6f4..fc1a0cd87be 100644 --- a/mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json @@ -3,7 +3,7 @@ "family": "project-explicit-false", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-1.json b/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-1.json index 58f8126e001..8068d4e21fb 100644 --- a/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-1.json +++ b/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-1.json @@ -3,7 +3,7 @@ "family": "relay.credential-rotation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-2.json b/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-2.json index af47ae27088..5d23602cb01 100644 --- a/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-2.json +++ b/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-2.json @@ -3,7 +3,7 @@ "family": "relay.credential-rotation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.provisionrelay-1.json b/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.provisionrelay-1.json index 3e43530e26b..f18c7b9bee8 100644 --- a/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.provisionrelay-1.json +++ b/mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.provisionrelay-1.json @@ -3,7 +3,7 @@ "family": "relay.credential-rotation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-1.json b/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-1.json index b909a3980a0..7b9a106276d 100644 --- a/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-1.json +++ b/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-1.json @@ -3,7 +3,7 @@ "family": "relay.direct-upgrade", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-2.json b/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-2.json index 97a2f5b57a7..d0a604a8cf8 100644 --- a/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-2.json +++ b/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-2.json @@ -3,7 +3,7 @@ "family": "relay.direct-upgrade", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.provisionrelay-1.json b/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.provisionrelay-1.json index f31c49e4347..0a5ff6b933d 100644 --- a/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.provisionrelay-1.json +++ b/mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.provisionrelay-1.json @@ -3,7 +3,7 @@ "family": "relay.direct-upgrade", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/matrix-relay.pairing-recovery-pairing.getendpoints-1.json b/mobile/rpc-foundation/goldens/matrix-relay.pairing-recovery-pairing.getendpoints-1.json index f9b57927e16..6d1daff5e4c 100644 --- a/mobile/rpc-foundation/goldens/matrix-relay.pairing-recovery-pairing.getendpoints-1.json +++ b/mobile/rpc-foundation/goldens/matrix-relay.pairing-recovery-pairing.getendpoints-1.json @@ -3,7 +3,7 @@ "family": "relay.pairing-recovery", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/matrix-session.browser-tab-create-browser.tabcreate-1.json b/mobile/rpc-foundation/goldens/matrix-session.browser-tab-create-browser.tabcreate-1.json index 91353afa64d..f1c04a310c8 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.browser-tab-create-browser.tabcreate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.browser-tab-create-browser.tabcreate-1.json @@ -3,7 +3,7 @@ "family": "session.browser-tab-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/matrix-session.content-create-files.createfile-1.json b/mobile/rpc-foundation/goldens/matrix-session.content-create-files.createfile-1.json index fa57c4e84b1..44d92581593 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.content-create-files.createfile-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.content-create-files.createfile-1.json @@ -3,7 +3,7 @@ "family": "session.content-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/matrix-session.content-create-files.open-1.json b/mobile/rpc-foundation/goldens/matrix-session.content-create-files.open-1.json index 21e1c4c3020..784482f03fa 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.content-create-files.open-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.content-create-files.open-1.json @@ -3,7 +3,7 @@ "family": "session.content-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/matrix-session.content-create-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-session.content-create-status.get-1.json index 4cbbef5e281..937553048c3 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.content-create-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.content-create-status.get-1.json @@ -3,7 +3,7 @@ "family": "session.content-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/matrix-session.content-create-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-session.content-create-worktree.show-1.json index 71c6e971dbe..9ea677cce7c 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.content-create-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.content-create-worktree.show-1.json @@ -3,7 +3,7 @@ "family": "session.content-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", @@ -166,6 +166,14 @@ } } }, + "37aff7a97a33": { + "createError": "The host sent a reply this app could not read (worktree.show)", + "creatingBrowser": false, + "creatingMarkdown": false, + "pendingBrowserFocusPageId": { + "$rpc": "null" + } + }, "3e7bfd4c59c3": { "name": "files.open#1", "ordinal": 7, @@ -341,14 +349,6 @@ "message": "Couldn't verify the SSH connection. Reconnect the host and try again." } }, - "7822fc8c989d": { - "createError": "Cannot read properties of undefined (reading 'worktree')", - "creatingBrowser": false, - "creatingMarkdown": false, - "pendingBrowserFocusPageId": { - "$rpc": "null" - } - }, "84c6d4a53548": { "name": "worktree.show#1", "ordinal": 3, @@ -384,13 +384,6 @@ } } }, - "8bebb9b2b076": { - "name": "toast", - "ordinal": 5, - "value": { - "message": "Cannot read properties of undefined (reading 'worktree')" - } - }, "9dc70bb3c6c3": { "name": "worktree.show#1", "ordinal": 3, @@ -555,12 +548,11 @@ "$rpc": "null" } }, - "d7217a17cb5c": { - "createError": "Cannot read properties of null (reading 'worktree')", - "creatingBrowser": false, - "creatingMarkdown": false, - "pendingBrowserFocusPageId": { - "$rpc": "null" + "d5e6e519e4a5": { + "name": "toast", + "ordinal": 5, + "value": { + "message": "The host sent a reply this app could not read (worktree.show)" } }, "e2ba4604deae": { @@ -620,13 +612,6 @@ "$rpc": "undefined" } }, - "f02973af22c4": { - "name": "toast", - "ordinal": 5, - "value": { - "message": "Cannot read properties of null (reading 'worktree')" - } - }, "f1279bf9f173": { "name": "files.createFile#1", "ordinal": 6, @@ -656,8 +641,8 @@ "settlements": { "markdown": "eb79a9b3682a" }, - "state": "7822fc8c989d", - "effects": ["8bebb9b2b076"] + "state": "37aff7a97a33", + "effects": ["d5e6e519e4a5"] } }, { @@ -668,8 +653,8 @@ "settlements": { "markdown": "eb79a9b3682a" }, - "state": "d7217a17cb5c", - "effects": ["f02973af22c4"] + "state": "37aff7a97a33", + "effects": ["d5e6e519e4a5"] } }, { diff --git a/mobile/rpc-foundation/goldens/matrix-session.create-terminal-session.tabs.createterminal-1.json b/mobile/rpc-foundation/goldens/matrix-session.create-terminal-session.tabs.createterminal-1.json index 16d14551029..4f237e17ce4 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.create-terminal-session.tabs.createterminal-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.create-terminal-session.tabs.createterminal-1.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/matrix-session.create-terminal-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-session.create-terminal-terminal.send-1.json index 663e07358c5..a9e337d8364 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.create-terminal-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.create-terminal-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/matrix-session.diff-notes-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-session.diff-notes-worktree.show-1.json index 292dbb3f07d..623d827695a 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.diff-notes-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.diff-notes-worktree.show-1.json @@ -3,7 +3,7 @@ "family": "session.diff-notes", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.diff-review-actions-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-session.diff-review-actions-worktree.set-1.json index 764d8927904..79cec039bdb 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.diff-review-actions-worktree.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.diff-review-actions-worktree.set-1.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/matrix-session.diff-review-base-ref-show.json b/mobile/rpc-foundation/goldens/matrix-session.diff-review-base-ref-show.json index 675a69973dd..bdd1bf2cf58 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.diff-review-base-ref-show.json +++ b/mobile/rpc-foundation/goldens/matrix-session.diff-review-base-ref-show.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.diff-review-git.branchcompare-1.json b/mobile/rpc-foundation/goldens/matrix-session.diff-review-git.branchcompare-1.json index 5c3e408d40c..0fed2535239 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.diff-review-git.branchcompare-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.diff-review-git.branchcompare-1.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.diff-review-git.status-1.json b/mobile/rpc-foundation/goldens/matrix-session.diff-review-git.status-1.json index f074448b367..1283e269962 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.diff-review-git.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.diff-review-git.status-1.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.diff-review-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.diff-review-repo.list-1.json index e7ea70f2a27..f2c7d076bf0 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.diff-review-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.diff-review-repo.list-1.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.diff-review-review-show.json b/mobile/rpc-foundation/goldens/matrix-session.diff-review-review-show.json index e8c15e75a18..1428f97fe04 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.diff-review-review-show.json +++ b/mobile/rpc-foundation/goldens/matrix-session.diff-review-review-show.json @@ -3,7 +3,7 @@ "family": "session.diff-review", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-files.read-1.json b/mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-files.read-1.json new file mode 100644 index 00000000000..572bfd06aa8 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-files.read-1.json @@ -0,0 +1,619 @@ +{ + "operation": "session.tab-documents", + "family": "session.markdown-disk-fallback", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", + "scenarioSha256": "c4b6c9c21433876e2bb3822f289627e7c5607830559289d35cae066ff895fc23", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1814b29b1212": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "232de05393a9": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "26b8fd580c90": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "411d336833d5": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "44abbe2af7aa": { + "name": "markdown.readTab#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"markdown.readTab\",\"params\":{\"worktree\":\"id:workspace-1\",\"tabId\":\"tab-md\"}}" + }, + "455401ec3b87": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "5d8fad85cc4d": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "767ef7fdc9b3": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "877720375363": { + "file": {}, + "markdown": { + "tab-md": { + "message": "Couldn't load markdown", + "status": "error" + } + } + }, + "c681a5b94b73": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "c6cea5310098": { + "file": {}, + "markdown": { + "tab-md": { + "baseVersion": "", + "content": "# disk", + "editable": false, + "isDirty": false, + "localContent": "# disk", + "readOnlyReason": "Editing needs Orca desktop running.", + "stale": false, + "status": "ready" + } + } + }, + "d4be01606497": { + "name": "files.read#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + }, + "d59404d25d6c": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "byteLength": 6, + "content": "# disk", + "truncated": false + } + } + } + }, + "e258dafc91f1": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "renderer_unavailable", + "message": "Renderer unavailable" + }, + "id": "frame-1", + "ok": false + } + } + }, + "e4cc82795cc4": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "e6ef1892cf5d": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "matrix-session.markdown-disk-fallback-files.read-1", + "checkpoints": [ + { + "id": "session-markdown-disk-read.normal:fell-back", + "observation": { + "sender": ["e258dafc91f1", "d59404d25d6c"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "c6cea5310098", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.result-absent:fell-back", + "observation": { + "sender": ["e258dafc91f1", "26b8fd580c90"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.result-null:fell-back", + "observation": { + "sender": ["e258dafc91f1", "232de05393a9"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.inner-ok-missing:fell-back", + "observation": { + "sender": ["e258dafc91f1", "c681a5b94b73"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.inner-false-string-error:fell-back", + "observation": { + "sender": ["e258dafc91f1", "e4cc82795cc4"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.inner-false-object-error:fell-back", + "observation": { + "sender": ["e258dafc91f1", "411d336833d5"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.outer-refused:fell-back", + "observation": { + "sender": ["e258dafc91f1", "5d8fad85cc4d"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.outer-refused-no-message:fell-back", + "observation": { + "sender": ["e258dafc91f1", "1814b29b1212"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.method-not-found:fell-back", + "observation": { + "sender": ["e258dafc91f1", "767ef7fdc9b3"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.transport-rejection:fell-back", + "observation": { + "sender": ["e258dafc91f1", "e6ef1892cf5d"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.transport-rejection-no-message:fell-back", + "observation": { + "sender": ["e258dafc91f1", "455401ec3b87"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-markdown.readtab-1.json b/mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-markdown.readtab-1.json new file mode 100644 index 00000000000..889e6c9ebdf --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-markdown.readtab-1.json @@ -0,0 +1,581 @@ +{ + "operation": "session.tab-documents", + "family": "session.markdown-disk-fallback", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", + "scenarioSha256": "42ddfcc6c8cac61e5891b72fbac9d04763fa9c52d0cb4f44e1044714a8a1ee77", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0d6b0adc036f": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "content": "# served", + "editable": true, + "isDirty": false, + "version": "v1" + } + } + } + }, + "11d4233470ab": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "27c8f1a219da": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "30ca4532d665": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "3e3efaab3a20": { + "file": {}, + "markdown": { + "tab-md": { + "baseVersion": "v1", + "content": "# served", + "editable": true, + "isDirty": false, + "localContent": "# served", + "readOnlyReason": { + "$rpc": "undefined" + }, + "stale": false, + "status": "ready" + } + } + }, + "3f9b64bce472": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "44abbe2af7aa": { + "name": "markdown.readTab#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"markdown.readTab\",\"params\":{\"worktree\":\"id:workspace-1\",\"tabId\":\"tab-md\"}}" + }, + "4b881f02b557": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "7316598fc7ff": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "877720375363": { + "file": {}, + "markdown": { + "tab-md": { + "message": "Couldn't load markdown", + "status": "error" + } + } + }, + "8f5af40a86f8": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "9678ee74c12b": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "a2bc1f147779": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "dec5db38cd1c": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "matrix-session.markdown-disk-fallback-markdown.readtab-1", + "checkpoints": [ + { + "id": "session-markdown-disk-read.normal:fell-back", + "observation": { + "sender": ["0d6b0adc036f"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "3e3efaab3a20", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.result-absent:fell-back", + "observation": { + "sender": ["30ca4532d665"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.result-null:fell-back", + "observation": { + "sender": ["7316598fc7ff"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.inner-ok-missing:fell-back", + "observation": { + "sender": ["dec5db38cd1c"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.inner-false-string-error:fell-back", + "observation": { + "sender": ["a2bc1f147779"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.inner-false-object-error:fell-back", + "observation": { + "sender": ["9678ee74c12b"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.outer-refused:fell-back", + "observation": { + "sender": ["11d4233470ab"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.outer-refused-no-message:fell-back", + "observation": { + "sender": ["4b881f02b557"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.method-not-found:fell-back", + "observation": { + "sender": ["27c8f1a219da"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.transport-rejection:fell-back", + "observation": { + "sender": ["8f5af40a86f8"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + }, + { + "id": "session-markdown-disk-read.transport-rejection-no-message:fell-back", + "observation": { + "sender": ["3f9b64bce472"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "877720375363", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-session.markdown-save-markdown.savetab-1.json b/mobile/rpc-foundation/goldens/matrix-session.markdown-save-markdown.savetab-1.json index efe0539ffba..b22420593a7 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.markdown-save-markdown.savetab-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.markdown-save-markdown.savetab-1.json @@ -3,7 +3,7 @@ "family": "session.markdown-save", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json index c7ddee8776a..d6a66ef187e 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json @@ -3,7 +3,7 @@ "family": "session.native-chat-page", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "60ce67f66134d6385aa7fb47f2135436a7b3ce0221e3b2514bea65e06dba5518", diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json index dffa854d0a1..ec3c0aa305f 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json @@ -3,7 +3,7 @@ "family": "session.native-chat-page", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "60ce67f66134d6385aa7fb47f2135436a7b3ce0221e3b2514bea65e06dba5518", diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json index 81cd1db8acd..90b310931e3 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json @@ -3,7 +3,7 @@ "family": "session.native-chat-page", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "60ce67f66134d6385aa7fb47f2135436a7b3ce0221e3b2514bea65e06dba5518", diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-readability-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-readability-repo.list-1.json index 57dab7fda46..8426ae5057d 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-readability-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-readability-repo.list-1.json @@ -3,7 +3,7 @@ "family": "session.native-chat-readability", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-orchestration.workerterminaluserinput-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-orchestration.workerterminaluserinput-1.json index eb8c4567ac6..10d0dcba8f2 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-orchestration.workerterminaluserinput-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-orchestration.workerterminaluserinput-1.json @@ -3,7 +3,7 @@ "family": "session.native-chat-stop", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-1.json index 08122935f57..a26fe07d343 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "session.native-chat-stop", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-2.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-2.json index f80eb23436d..c28ba3987c8 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-2.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-2.json @@ -3,7 +3,7 @@ "family": "session.native-chat-stop", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.branchcompare-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.branchcompare-1.json index b544b481498..c585a821bb3 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.branchcompare-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.branchcompare-1.json @@ -3,7 +3,7 @@ "family": "session.pr-branch-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.status-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.status-1.json index c412f6b5a21..6085103ee5e 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.status-1.json @@ -3,7 +3,7 @@ "family": "session.pr-branch-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-repo.list-1.json index 790106db047..f7fd1adb4ff 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-repo.list-1.json @@ -3,7 +3,7 @@ "family": "session.pr-branch-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-worktree.show-1.json index f65b9dc6666..e47fadcc518 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-worktree.show-1.json @@ -3,7 +3,7 @@ "family": "session.pr-branch-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prchecks-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prchecks-1.json index e91d503fb2f..48c2cd863ef 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prchecks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prchecks-1.json @@ -3,7 +3,7 @@ "family": "session.pr-sidebar", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prforbranch-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prforbranch-1.json index 236bbe89b9a..4eb0fc51ea0 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prforbranch-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prforbranch-1.json @@ -3,7 +3,7 @@ "family": "session.pr-sidebar", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-hostedreview.forbranch-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-hostedreview.forbranch-1.json index fea60749920..b8feccb506a 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-hostedreview.forbranch-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-hostedreview.forbranch-1.json @@ -3,7 +3,7 @@ "family": "session.pr-sidebar", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-worktree.show-1.json index 08927f9c3ca..57f913b1766 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-worktree.show-1.json @@ -3,7 +3,7 @@ "family": "session.pr-sidebar", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-session.tabs.createterminal-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-session.tabs.createterminal-1.json index 04c2514219e..cf3815b4ab0 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-session.tabs.createterminal-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-session.tabs.createterminal-1.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-terminal.send-1.json index f957f62dd0e..6496fc23230 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/matrix-session.review-branch-diff-git.branchdiff-1.json b/mobile/rpc-foundation/goldens/matrix-session.review-branch-diff-git.branchdiff-1.json index 8ce27c2dd85..9cdcf1f43be 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.review-branch-diff-git.branchdiff-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.review-branch-diff-git.branchdiff-1.json @@ -3,7 +3,7 @@ "family": "session.review-branch-diff", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-1.json b/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-1.json index 215735fe899..e8e190b32f5 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-1.json @@ -3,7 +3,7 @@ "family": "session.review-file-diff", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-2.json b/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-2.json index 20bb3f90804..78194b5fc50 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-2.json +++ b/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-2.json @@ -3,7 +3,7 @@ "family": "session.review-file-diff", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-3.json b/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-3.json index cb2bf015528..cb57e9b87c8 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-3.json +++ b/mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-3.json @@ -3,7 +3,7 @@ "family": "session.review-file-diff", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.discard-1.json b/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.discard-1.json index 83372cf271a..69103951e33 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.discard-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.discard-1.json @@ -3,7 +3,7 @@ "family": "session.review-git-mutations", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-1.json b/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-1.json index 2321a61cf6e..41bdb40f6c1 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-1.json @@ -3,7 +3,7 @@ "family": "session.review-git-mutations", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-2.json b/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-2.json index 617b20b7ed1..83a13fc2172 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-2.json +++ b/mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-2.json @@ -3,7 +3,7 @@ "family": "session.review-git-mutations", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/matrix-session.review-send-sheet-session.tabs.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.review-send-sheet-session.tabs.list-1.json index 81c9c2f3af9..e6a2456adf8 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.review-send-sheet-session.tabs.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.review-send-sheet-session.tabs.list-1.json @@ -3,7 +3,7 @@ "family": "session.review-send-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-1.json b/mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-1.json index 89cdce9f5b2..9938c5feb91 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-1.json @@ -3,7 +3,7 @@ "family": "session.startup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "6b08c394e37fd572cf63a4c11934247d379008f11117aa5af33b2926fbd32d1e", diff --git a/mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-2.json b/mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-2.json index 11248526070..eba51894d02 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-2.json +++ b/mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-2.json @@ -3,7 +3,7 @@ "family": "session.startup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "6b08c394e37fd572cf63a4c11934247d379008f11117aa5af33b2926fbd32d1e", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-activation-session.tabs.activate-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-activation-session.tabs.activate-1.json index 848bff50218..35e05041f75 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-activation-session.tabs.activate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-activation-session.tabs.activate-1.json @@ -3,7 +3,7 @@ "family": "session.tab-activation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-activation-terminal.focus-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-activation-terminal.focus-1.json index 3732bf509f8..b77e9434391 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-activation-terminal.focus-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-activation-terminal.focus-1.json @@ -3,7 +3,7 @@ "family": "session.tab-activation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-close-session-session.tabs.close-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-close-session-session.tabs.close-1.json index 3ff6d8f1b31..aa7e11b10c9 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-close-session-session.tabs.close-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-close-session-session.tabs.close-1.json @@ -3,7 +3,7 @@ "family": "session.tab-close-session", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-close-terminal.close-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-close-terminal.close-1.json index c2804effb53..f32a7834ecc 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-close-terminal.close-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-close-terminal.close-1.json @@ -3,7 +3,7 @@ "family": "session.tab-close", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-documents-markdown.readtab-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-documents-markdown.readtab-1.json index ea1bb009889..895f01ac4e7 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-documents-markdown.readtab-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-documents-markdown.readtab-1.json @@ -3,7 +3,7 @@ "family": "session.tab-documents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-rename-terminal.rename-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-rename-terminal.rename-1.json index 534ad58342f..5dcdc830c19 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-rename-terminal.rename-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-rename-terminal.rename-1.json @@ -3,7 +3,7 @@ "family": "session.tab-rename", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json index e8b7762306d..4d4a36f6fac 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json @@ -3,7 +3,7 @@ "family": "session.tab-reveal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json index 3cbcb0653de..8ea5bd2be60 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json @@ -3,7 +3,7 @@ "family": "session.tab-reveal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tabs-stream-health-session.tabs.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.tabs-stream-health-session.tabs.list-1.json index 9f8a0e9fbb9..6fd108933b9 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tabs-stream-health-session.tabs.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tabs-stream-health-session.tabs.list-1.json @@ -3,7 +3,7 @@ "family": "session.tabs-stream-health", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-display-mode-terminal.setdisplaymode-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-display-mode-terminal.setdisplaymode-1.json index b115ba054ae..f4b4a6576e7 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-display-mode-terminal.setdisplaymode-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-display-mode-terminal.setdisplaymode-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-display-mode", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9e90ad39a8d4257adf30166757a6364c4710dc3ae9f06365f80a3dd8f1c94d89", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-orchestration.workerterminaluserinput-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-orchestration.workerterminaluserinput-1.json index a26318e0ef3..f1c39daf967 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-orchestration.workerterminaluserinput-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-orchestration.workerterminaluserinput-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-gesture-input", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9d119d5ec320e2538105d6ff673b9f4b8e3decbe46527947489dffbcf2ac0472", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.clearbuffer-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.clearbuffer-1.json index 4912ff790b6..eec62b76a58 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.clearbuffer-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.clearbuffer-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-gesture-input", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9d119d5ec320e2538105d6ff673b9f4b8e3decbe46527947489dffbcf2ac0472", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.send-1.json index 83649978744..ca5f852b678 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-gesture-input", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9d119d5ec320e2538105d6ff673b9f4b8e3decbe46527947489dffbcf2ac0472", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-orchestration.workerterminaluserinput-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-orchestration.workerterminaluserinput-1.json index 2d054b6c055..782caa89cbd 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-orchestration.workerterminaluserinput-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-orchestration.workerterminaluserinput-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-input-send", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-terminal.send-1.json index d006f7709f7..dc7f2ebfc0b 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-input-send", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-inventory-terminal.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-inventory-terminal.list-1.json index edf15c057ba..c6bad41ea5c 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-inventory-terminal.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-inventory-terminal.list-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-orchestration.workerterminaluserinput-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-orchestration.workerterminaluserinput-1.json index dd73cd8fcec..a5c0c85b66a 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-orchestration.workerterminaluserinput-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-orchestration.workerterminaluserinput-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-settings.get-1.json index 3298c79fe70..2060916a4bf 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-settings.get-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-terminal.send-1.json index 90c41a4cfc1..a738ed53b3d 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.terminal-paste-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "session.terminal-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/matrix-session.worktree-connection-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.worktree-connection-repo.list-1.json index 36b19989309..fb0ac449993 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.worktree-connection-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.worktree-connection-repo.list-1.json @@ -3,7 +3,7 @@ "family": "session.worktree-connection", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/matrix-session.worktree-connection-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-session.worktree-connection-settings.get-1.json index 655326b952c..0aa1365c328 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.worktree-connection-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.worktree-connection-settings.get-1.json @@ -3,7 +3,7 @@ "family": "session.worktree-connection", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json index c21a96daec0..9bb778e5dbc 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json index a221e9d3e9f..d5e47a92f43 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json index d7359707f9d..0fc5441fb70 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json b/mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json index deb4ea6ce8e..7b84a9ee3f1 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json @@ -3,7 +3,7 @@ "family": "settings-best-effort", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json index 4b237295a5c..317d05e89cd 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json index 44e85e8a206..1d46b8cb543 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json index f687bb7e1ce..4e6900f9c3c 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json index b716a340be2..0632e412cea 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-preflight.detectagents-1.json b/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-preflight.detectagents-1.json index 908fb93784b..26e30bda713 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-preflight.detectagents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-preflight.detectagents-1.json @@ -3,7 +3,7 @@ "family": "settings.new-tab-local-agents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-repo.list-1.json index b8f18a501bf..2b59c43016e 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-repo.list-1.json @@ -3,7 +3,7 @@ "family": "settings.new-tab-local-agents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-settings.get-1.json index 45bf3e870e7..dd3261fac6d 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.new-tab-local-agents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.getterminalquickcommands-1.json b/mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.getterminalquickcommands-1.json index ee04d1b2487..1c4f6e8d449 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.getterminalquickcommands-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.getterminalquickcommands-1.json @@ -3,7 +3,7 @@ "family": "settings.quick-commands", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.updateterminalquickcommands-1.json b/mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.updateterminalquickcommands-1.json index 221e6e64551..93de68eb77b 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.updateterminalquickcommands-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.updateterminalquickcommands-1.json @@ -3,7 +3,7 @@ "family": "settings.quick-commands", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json index 0a1f0749ffb..04fc071c991 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json index 804fbc95572..1c946d451b4 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json index 8f4483b34b3..e573f898896 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json index 4be80938346..beebf240ecc 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json index f29e04b6e0a..3a33228ba13 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", @@ -457,6 +457,17 @@ } } }, + "9cd99bc29688": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (folderWorkspace.list)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "a2feaaab3fcf": { "name": "folderWorkspace.list#1", "ordinal": 2, @@ -723,7 +734,7 @@ "1bfeba989b20" ], "settlements": { - "load": "3c70da5d6d8e" + "load": "9cd99bc29688" }, "state": "44136fa355b3", "effects": [] @@ -771,7 +782,7 @@ "1bfeba989b20" ], "settlements": { - "load": "3c70da5d6d8e" + "load": "9cd99bc29688" }, "state": "44136fa355b3", "effects": [] diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json index b5a1e91b375..e5096bd5609 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", @@ -590,6 +590,17 @@ } } }, + "c884ec49315c": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (projectGroup.list)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "cabca99619ed": { "name": "repo.list#1", "ordinal": 6, @@ -723,7 +734,7 @@ "1bfeba989b20" ], "settlements": { - "load": "3c70da5d6d8e" + "load": "c884ec49315c" }, "state": "44136fa355b3", "effects": [] @@ -771,7 +782,7 @@ "1bfeba989b20" ], "settlements": { - "load": "3c70da5d6d8e" + "load": "c884ec49315c" }, "state": "44136fa355b3", "effects": [] diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json index 91aefa1fa88..5412f0aa704 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", @@ -133,13 +133,13 @@ "ordinal": 7, "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"folderWorkspace.list\"}" }, - "2381a3fe154e": { + "28b1c60e88cb": { "status": "rejected", "startedAt": 0, "settledAt": 0, "error": { - "category": "TypeError", - "message": "Cannot read properties of undefined (reading 'repos')", + "category": "Error", + "message": "The host sent a reply this app could not read (repo.list)", "isRpcDeliveryUnknown": false } }, @@ -211,16 +211,6 @@ "ordinal": 8, "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"projectGroup.list\"}" }, - "63dfbb6942f2": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "TypeError", - "message": "Cannot read properties of null (reading 'repos')", - "isRpcDeliveryUnknown": false - } - }, "6af91c1da122": { "name": "worktree.ps#1", "ordinal": 5, @@ -793,7 +783,7 @@ "1bfeba989b20" ], "settlements": { - "load": "2381a3fe154e" + "load": "28b1c60e88cb" }, "state": "44136fa355b3", "effects": [] @@ -841,7 +831,7 @@ "1bfeba989b20" ], "settlements": { - "load": "63dfbb6942f2" + "load": "28b1c60e88cb" }, "state": "44136fa355b3", "effects": [] @@ -889,7 +879,7 @@ "1bfeba989b20" ], "settlements": { - "load": "3c70da5d6d8e" + "load": "28b1c60e88cb" }, "state": "44136fa355b3", "effects": [] @@ -937,7 +927,7 @@ "1bfeba989b20" ], "settlements": { - "load": "3c70da5d6d8e" + "load": "28b1c60e88cb" }, "state": "44136fa355b3", "effects": [] @@ -985,7 +975,7 @@ "1bfeba989b20" ], "settlements": { - "load": "3c70da5d6d8e" + "load": "28b1c60e88cb" }, "state": "44136fa355b3", "effects": [] diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json index c8b5e6f5c7d..6a0c4b2d20d 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json index 653a3f0f2cb..5f566166d7c 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", @@ -399,6 +399,17 @@ } } }, + "7fb1c6f9f3ae": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "RpcIncompatibleReplyError", + "message": "The host sent a reply this app could not read (worktree.ps)", + "isRpcDeliveryUnknown": false, + "code": "incompatible_reply" + } + }, "82132b79b8f7": { "name": "settings.get#1", "ordinal": 9, @@ -719,7 +730,7 @@ "1bfeba989b20" ], "settlements": { - "load": "0620c0819077" + "load": "7fb1c6f9f3ae" }, "state": "44136fa355b3", "effects": [] @@ -743,7 +754,7 @@ "1bfeba989b20" ], "settlements": { - "load": "0620c0819077" + "load": "7fb1c6f9f3ae" }, "state": "44136fa355b3", "effects": [] diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json index b2086bae0aa..f3cca3e83dc 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json index 94569d2b169..24a6b327ea8 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json index cddfb049564..9ac54d8234b 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json index af687bf83bf..25355ba2aa6 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json index f083affadd8..3d0950d887d 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json index aa2e4e4f1d3..8edc55a3cae 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.task-workspace-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json index 6215b297e59..4222003e02c 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json @@ -3,7 +3,7 @@ "family": "settings.task-workspace-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json index 609a0d70ee5..9648600cd0b 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.task-workspace", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json index f0381382c07..78bd94371e2 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json index b0f0df3f73c..aa38a079c17 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json index 3491f08db14..6d6ff546eb3 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json index 628a7c47d44..2433a222fb3 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json index cd67ff0b6eb..5190e2f1243 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json @@ -3,7 +3,7 @@ "family": "settings.workspace-submit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json index c9eecb78f73..54d3406c0c3 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json @@ -3,7 +3,7 @@ "family": "speech.dictation-chunk", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json index 7242ab948fa..38352cc271e 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json @@ -3,7 +3,7 @@ "family": "speech.dictation-session", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json index 34cb9c1c8b5..5e21ae38877 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json @@ -3,7 +3,7 @@ "family": "speech.dictation-session", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json index 97dd9c1b69c..f1987472c99 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json @@ -3,7 +3,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json index 5962b8feb8c..5419fa7776d 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json @@ -3,7 +3,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.dictation.setup-1.json b/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.dictation.setup-1.json index 7dd4a487bbf..e68efcd8310 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.dictation.setup-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.dictation.setup-1.json @@ -3,7 +3,7 @@ "family": "speech.setup-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", @@ -85,35 +85,14 @@ "ordinal": 6, "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"speech.models.delete\",\"params\":{\"modelId\":\"whisper-small\"}}" }, - "301151228fa3": { - "status": "fulfilled", + "2d0263e221bc": { + "status": "rejected", "startedAt": 0, "settledAt": 0, - "value": { - "error": "refused" - } - }, - "3033fa217374": { - "configure": { - "error": "inner refused", - "ok": false - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" + "error": { + "category": "Error", + "message": "The host sent a reply this app could not read (speech.dictation.setup)", + "isRpcDeliveryUnknown": false } }, "32a7c0ae7918": { @@ -169,31 +148,6 @@ } } }, - "43c4c7a19f7e": { - "configure": { - "error": { - "message": "inner refused" - }, - "ok": false - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, "4f793e38be31": { "name": "speech.models.list#1", "ordinal": 2, @@ -318,26 +272,6 @@ } } }, - "7af31590ded9": { - "configure": "started", - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, "7c5b27891a8f": { "configure": { "enabled": true, @@ -401,15 +335,6 @@ } } }, - "9f00dd54ba64": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": "inner refused", - "ok": false - } - }, "a2879fd6371d": { "status": "fulfilled", "startedAt": 0, @@ -510,39 +435,6 @@ "isRpcDeliveryUnknown": true } }, - "a9c35a6f891b": { - "configure": { - "error": "refused" - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, - "ad8a954e879d": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "message": "inner refused" - }, - "ok": false - } - }, "adbb96fcc08c": { "name": "speech.models.download#1", "ordinal": 3, @@ -620,28 +512,6 @@ "isRpcDeliveryUnknown": false } }, - "be76d126a25c": { - "configure": { - "$rpc": "null" - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, "c2b954773835": { "name": "speech.dictation.setup#1", "ordinal": 7, @@ -741,14 +611,6 @@ "isRpcDeliveryUnknown": false } }, - "ee20a1dc39e7": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "$rpc": "null" - } - }, "efb9a676286c": { "delete": { "enabled": true, @@ -847,9 +709,9 @@ "list": "a2879fd6371d", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", - "configure": "eb79a9b3682a" + "configure": "2d0263e221bc" }, - "state": "7af31590ded9", + "state": "efb9a676286c", "effects": [] } }, @@ -862,9 +724,9 @@ "list": "a2879fd6371d", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", - "configure": "ee20a1dc39e7" + "configure": "2d0263e221bc" }, - "state": "be76d126a25c", + "state": "efb9a676286c", "effects": [] } }, @@ -877,9 +739,9 @@ "list": "a2879fd6371d", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", - "configure": "301151228fa3" + "configure": "2d0263e221bc" }, - "state": "a9c35a6f891b", + "state": "efb9a676286c", "effects": [] } }, @@ -892,9 +754,9 @@ "list": "a2879fd6371d", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", - "configure": "9f00dd54ba64" + "configure": "2d0263e221bc" }, - "state": "3033fa217374", + "state": "efb9a676286c", "effects": [] } }, @@ -907,9 +769,9 @@ "list": "a2879fd6371d", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", - "configure": "ad8a954e879d" + "configure": "2d0263e221bc" }, - "state": "43c4c7a19f7e", + "state": "efb9a676286c", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.delete-1.json b/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.delete-1.json index a783e91a5a0..fc2f788c2ad 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.delete-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.delete-1.json @@ -3,7 +3,7 @@ "family": "speech.setup-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", @@ -13,68 +13,11 @@ "projectionVersion": 2, "goldenFormatVersion": 5, "values": { - "0629aa17065f": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "error": { - "message": "inner refused" - }, - "ok": false - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, "0e61f11d0307": { "name": "speech.models.delete#1", "ordinal": 6, "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"speech.models.delete\",\"params\":{\"modelId\":\"whisper-small\"}}" }, - "1117d44df3f8": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": "started", - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, "1f67869c3be1": { "configure": { "enabled": true, @@ -137,14 +80,6 @@ } } }, - "301151228fa3": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": "refused" - } - }, "32a7c0ae7918": { "status": "rejected", "startedAt": 0, @@ -264,34 +199,6 @@ } } }, - "3eac8959f5ab": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "$rpc": "null" - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, "47237ace2093": { "name": "speech.models.delete#1", "ordinal": 5, @@ -474,15 +381,6 @@ "selectedModelId": "whisper-small" } }, - "9f00dd54ba64": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": "inner refused", - "ok": false - } - }, "a2879fd6371d": { "status": "fulfilled", "startedAt": 0, @@ -519,17 +417,6 @@ "isRpcDeliveryUnknown": false } }, - "ad8a954e879d": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "message": "inner refused" - }, - "ok": false - } - }, "adbb96fcc08c": { "name": "speech.models.download#1", "ordinal": 3, @@ -619,34 +506,6 @@ } } }, - "d32fe9752c54": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "error": "refused" - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, "d8c329a93e43": { "name": "speech.models.delete#1", "ordinal": 5, @@ -718,35 +577,6 @@ } } }, - "e2401fd120ea": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "error": "inner refused", - "ok": false - }, - "download": "started", - "list": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - } - }, "e35a372455e8": { "name": "speech.models.delete#1", "ordinal": 5, @@ -782,6 +612,16 @@ } } }, + "e5305075c4c9": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "The host sent a reply this app could not read (speech.models.delete)", + "isRpcDeliveryUnknown": false + } + }, "eb5c8d14744b": { "name": "speech.models.delete#1", "ordinal": 5, @@ -822,14 +662,6 @@ "$rpc": "undefined" } }, - "ee20a1dc39e7": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "$rpc": "null" - } - }, "fba26d61d9d2": { "name": "speech.dictation.setup#1", "ordinal": 8, @@ -872,10 +704,10 @@ "settlements": { "list": "a2879fd6371d", "download": "eb79a9b3682a", - "delete": "eb79a9b3682a", + "delete": "e5305075c4c9", "configure": "a2879fd6371d" }, - "state": "1117d44df3f8", + "state": "1f67869c3be1", "effects": [] } }, @@ -887,10 +719,10 @@ "settlements": { "list": "a2879fd6371d", "download": "eb79a9b3682a", - "delete": "ee20a1dc39e7", + "delete": "e5305075c4c9", "configure": "a2879fd6371d" }, - "state": "3eac8959f5ab", + "state": "1f67869c3be1", "effects": [] } }, @@ -902,10 +734,10 @@ "settlements": { "list": "a2879fd6371d", "download": "eb79a9b3682a", - "delete": "301151228fa3", + "delete": "e5305075c4c9", "configure": "a2879fd6371d" }, - "state": "d32fe9752c54", + "state": "1f67869c3be1", "effects": [] } }, @@ -917,10 +749,10 @@ "settlements": { "list": "a2879fd6371d", "download": "eb79a9b3682a", - "delete": "9f00dd54ba64", + "delete": "e5305075c4c9", "configure": "a2879fd6371d" }, - "state": "e2401fd120ea", + "state": "1f67869c3be1", "effects": [] } }, @@ -932,10 +764,10 @@ "settlements": { "list": "a2879fd6371d", "download": "eb79a9b3682a", - "delete": "ad8a954e879d", + "delete": "e5305075c4c9", "configure": "a2879fd6371d" }, - "state": "0629aa17065f", + "state": "1f67869c3be1", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.download-1.json b/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.download-1.json index 1701d568411..e1990ed11a1 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.download-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.download-1.json @@ -3,7 +3,7 @@ "family": "speech.setup-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.list-1.json b/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.list-1.json index 7f556798882..f773a3353bf 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.list-1.json @@ -3,7 +3,7 @@ "family": "speech.setup-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", @@ -18,79 +18,6 @@ "ordinal": 6, "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"speech.models.delete\",\"params\":{\"modelId\":\"whisper-small\"}}" }, - "1885e95050f7": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": "started" - }, - "26e00f0930ac": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "$rpc": "null" - } - }, - "26e98969da6c": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "error": "inner refused", - "ok": false - } - }, - "301151228fa3": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": "refused" - } - }, "32a7c0ae7918": { "status": "rejected", "startedAt": 0, @@ -144,29 +71,14 @@ } } }, - "4e80c1e9f058": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "error": { - "message": "inner refused" - }, - "ok": false + "44b46aebf7db": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "The host sent a reply this app could not read (speech.models.list)", + "isRpcDeliveryUnknown": false } }, "4f793e38be31": { @@ -362,28 +274,6 @@ "selectedModelId": "whisper-small" } }, - "8214effff7c5": { - "configure": { - "enabled": true, - "models": [ - { - "id": "whisper-small", - "name": "Small", - "status": "ready" - } - ], - "selectedModelId": "whisper-small" - }, - "delete": { - "enabled": true, - "models": [], - "selectedModelId": "whisper-small" - }, - "download": "started", - "list": { - "error": "refused" - } - }, "8a419aef0d30": { "name": "speech.models.list#1", "ordinal": 1, @@ -475,15 +365,6 @@ } } }, - "9f00dd54ba64": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": "inner refused", - "ok": false - } - }, "a1c6027c24c9": { "name": "speech.models.list#1", "ordinal": 1, @@ -541,17 +422,6 @@ "isRpcDeliveryUnknown": true } }, - "ad8a954e879d": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "message": "inner refused" - }, - "ok": false - } - }, "adbb96fcc08c": { "name": "speech.models.download#1", "ordinal": 3, @@ -752,14 +622,6 @@ "$rpc": "undefined" } }, - "ee20a1dc39e7": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "$rpc": "null" - } - }, "fba26d61d9d2": { "name": "speech.dictation.setup#1", "ordinal": 8, @@ -834,12 +696,12 @@ "sender": ["a1c6027c24c9", "adbb96fcc08c", "d8c329a93e43", "38ccf3e1658f"], "payloads": ["4f793e38be31", "694b34cbeee9", "0e61f11d0307", "fba26d61d9d2"], "settlements": { - "list": "eb79a9b3682a", + "list": "44b46aebf7db", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", "configure": "a2879fd6371d" }, - "state": "1885e95050f7", + "state": "8c2c55317f83", "effects": [] } }, @@ -849,12 +711,12 @@ "sender": ["fcf4e2c83b5a", "adbb96fcc08c", "d8c329a93e43", "38ccf3e1658f"], "payloads": ["4f793e38be31", "694b34cbeee9", "0e61f11d0307", "fba26d61d9d2"], "settlements": { - "list": "ee20a1dc39e7", + "list": "44b46aebf7db", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", "configure": "a2879fd6371d" }, - "state": "26e00f0930ac", + "state": "8c2c55317f83", "effects": [] } }, @@ -864,12 +726,12 @@ "sender": ["64e366a1c93a", "adbb96fcc08c", "d8c329a93e43", "38ccf3e1658f"], "payloads": ["4f793e38be31", "694b34cbeee9", "0e61f11d0307", "fba26d61d9d2"], "settlements": { - "list": "301151228fa3", + "list": "44b46aebf7db", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", "configure": "a2879fd6371d" }, - "state": "8214effff7c5", + "state": "8c2c55317f83", "effects": [] } }, @@ -879,12 +741,12 @@ "sender": ["8a419aef0d30", "adbb96fcc08c", "d8c329a93e43", "38ccf3e1658f"], "payloads": ["4f793e38be31", "694b34cbeee9", "0e61f11d0307", "fba26d61d9d2"], "settlements": { - "list": "9f00dd54ba64", + "list": "44b46aebf7db", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", "configure": "a2879fd6371d" }, - "state": "26e98969da6c", + "state": "8c2c55317f83", "effects": [] } }, @@ -894,12 +756,12 @@ "sender": ["94dfe144483b", "adbb96fcc08c", "d8c329a93e43", "38ccf3e1658f"], "payloads": ["4f793e38be31", "694b34cbeee9", "0e61f11d0307", "fba26d61d9d2"], "settlements": { - "list": "ad8a954e879d", + "list": "44b46aebf7db", "download": "eb79a9b3682a", "delete": "fc5fb77f49bb", "configure": "a2879fd6371d" }, - "state": "4e80c1e9f058", + "state": "8c2c55317f83", "effects": [] } }, diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.addprreviewcomment-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.addprreviewcomment-1.json index 1b5a18a57c0..9bfd006843e 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.addprreviewcomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.addprreviewcomment-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-checks-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.prfilecontents-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.prfilecontents-1.json index 65269a47b84..0cc70ca28ed 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.prfilecontents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.prfilecontents-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-checks-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.rerunprchecks-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.rerunprchecks-1.json index 8c6cc35dac7..2c0d1191f59 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.rerunprchecks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.rerunprchecks-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-checks-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.resolvereviewthread-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.resolvereviewthread-1.json index da6a63c6225..c844f6d84b7 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.resolvereviewthread-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.resolvereviewthread-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-checks-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.setprfileviewed-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.setprfileviewed-1.json index eaa3d6d4b87..b38705e3fc0 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.setprfileviewed-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.setprfileviewed-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-checks-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-github-github.addissuecomment-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-github-github.addissuecomment-1.json index 83dd09774a4..2c347580e4e 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-github-github.addissuecomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-github-github.addissuecomment-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-comment-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-gitlab.addissuecomment-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-gitlab.addissuecomment-1.json index ea1968e8cbb..f70aa0784aa 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-gitlab.addissuecomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-gitlab.addissuecomment-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-comment-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-mr-gitlab.addmrcomment-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-mr-gitlab.addmrcomment-1.json index f9d58525287..05c987158a4 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-mr-gitlab.addmrcomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-mr-gitlab.addmrcomment-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-comment-gitlab-mr", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-github-github.workitemdetails-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-github-github.workitemdetails-1.json index 03007d8522f..fe4ae7760da 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-github-github.workitemdetails-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-github-github.workitemdetails-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-gitlab-gitlab.workitemdetails-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-gitlab-gitlab.workitemdetails-1.json index 97fae23ab2a..738700fa618 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-gitlab-gitlab.workitemdetails-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-gitlab-gitlab.workitemdetails-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.getissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.getissue-1.json index bdded9db611..b2bc9a7c466 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.getissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.getissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-linear", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.issuecomments-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.issuecomments-1.json index 1e705fe4cec..1c9d549f4ee 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.issuecomments-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.issuecomments-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-linear", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listassignableusers-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listassignableusers-1.json index a67ada78a02..cbfecde65a7 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listassignableusers-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listassignableusers-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "58ea1553e04017c993aea4753aace41ee664705a3fdb3b18569c5a9d7968cf06", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listlabels-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listlabels-1.json index bbcc1202777..d0a8beef27f 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listlabels-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listlabels-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "58ea1553e04017c993aea4753aace41ee664705a3fdb3b18569c5a9d7968cf06", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-merge-gitlab-gitlab.mergemr-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-merge-gitlab-gitlab.mergemr-1.json index b5be03abfa4..9ffa4fd09f5 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-merge-gitlab-gitlab.mergemr-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-merge-gitlab-gitlab.mergemr-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-merge-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-github-github.updatepr-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-github-github.updatepr-1.json index 12c353ab736..026535f9c5b 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-github-github.updatepr-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-github-github.updatepr-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-metadata-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-gitlab.updateissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-gitlab.updateissue-1.json index 9997ac89918..5df6ab8a123 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-gitlab.updateissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-gitlab.updateissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-metadata-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-mr-gitlab.updatemr-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-mr-gitlab.updatemr-1.json index f7146e96a49..4acb4320ea2 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-mr-gitlab.updatemr-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-mr-gitlab.updatemr-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-metadata-gitlab-mr", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addissuecomment-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addissuecomment-1.json index 6b1e2e889e2..578685a5fb8 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addissuecomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addissuecomment-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-reply-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addprreviewcommentreply-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addprreviewcommentreply-1.json index 2ad97883d9d..d1a1f1075ee 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addprreviewcommentreply-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addprreviewcommentreply-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-reply-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.mergepr-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.mergepr-1.json index 297d62b6bf8..8bf181851b8 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.mergepr-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.mergepr-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-reply-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-linear.updateissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-linear.updateissue-1.json index b20f92c54f3..4e4927dcef1 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-linear.updateissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-linear.updateissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-reply-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.prchecks-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.prchecks-1.json index 6c713ef0234..e2d2832b5ee 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.prchecks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.prchecks-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-review-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.requestprreviewers-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.requestprreviewers-1.json index 20da53c66d4..fb13f486e88 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.requestprreviewers-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.requestprreviewers-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-review-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-github.updateissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-github.updateissue-1.json index bf722b59b8f..3dab0676dbc 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-github.updateissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-github.updateissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-status-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-gitlab.updateissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-gitlab.updateissue-1.json index 3292e750f9f..bcb4ef849d4 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-gitlab.updateissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-gitlab.updateissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-status-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-mr-gitlab.updatemrstate-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-mr-gitlab.updatemrstate-1.json index 5237d617236..a2058f3839c 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-mr-gitlab.updatemrstate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-mr-gitlab.updatemrstate-1.json @@ -3,7 +3,7 @@ "family": "tasks.item-status-gitlab-mr", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.linear-connect-linear.connect-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.linear-connect-linear.connect-1.json index 2a3c6b58fb5..05eddb607b6 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.linear-connect-linear.connect-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.linear-connect-linear.connect-1.json @@ -3,7 +3,7 @@ "family": "tasks.linear-connect", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.addissuecomment-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.addissuecomment-1.json index 3652d6ab0b7..e872951bdda 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.addissuecomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.addissuecomment-1.json @@ -3,7 +3,7 @@ "family": "tasks.linear-item", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.createissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.createissue-1.json index 7b8c01df292..30252ff7779 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.createissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.createissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.linear-item", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.getissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.getissue-1.json index a08c323c624..35ca7259162 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.getissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.getissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.linear-item", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.listteams-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.listteams-1.json index 50c3d200468..585a0ce1a53 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.listteams-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.listteams-1.json @@ -3,7 +3,7 @@ "family": "tasks.linear-team-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "58ea1553e04017c993aea4753aace41ee664705a3fdb3b18569c5a9d7968cf06", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.teamstates-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.teamstates-1.json index 46bcc3cd225..a331c042414 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.teamstates-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.teamstates-1.json @@ -3,7 +3,7 @@ "family": "tasks.linear-team-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "58ea1553e04017c993aea4753aace41ee664705a3fdb3b18569c5a9d7968cf06", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json index 63efaca3acd..e568ed7deb2 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json @@ -3,7 +3,7 @@ "family": "tasks.paste-lookup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json index cd8589b0243..09a8623e12d 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json @@ -3,7 +3,7 @@ "family": "tasks.paste-lookup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json index 23fccbb0d23..3a9a431b0d3 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json @@ -3,7 +3,7 @@ "family": "tasks.paste-lookup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json index 713a65f8e21..42cc25ea0f4 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json @@ -3,7 +3,7 @@ "family": "tasks.paste-lookup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listaccessible-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listaccessible-1.json index 761c9022351..e72e3708c06 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listaccessible-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listaccessible-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-board-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c4272385ed3b0de4feab38de9e4f6363ecd6317fdd4de47f76a98eb18abaf371", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-1.json index 38086946480..56b5c985e08 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-board-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c4272385ed3b0de4feab38de9e4f6363ecd6317fdd4de47f76a98eb18abaf371", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-2.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-2.json index 0be2ce938b4..43ebd12480b 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-2.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-2.json @@ -3,7 +3,7 @@ "family": "tasks.project-board-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c4272385ed3b0de4feab38de9e4f6363ecd6317fdd4de47f76a98eb18abaf371", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.resolveref-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.resolveref-1.json index e5188852085..1df9758b390 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.resolveref-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.resolveref-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-board-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c4272385ed3b0de4feab38de9e4f6363ecd6317fdd4de47f76a98eb18abaf371", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.viewtable-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.viewtable-1.json index 6dfb3ebf8f0..2d571a50b27 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.viewtable-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.viewtable-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-board-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c4272385ed3b0de4feab38de9e4f6363ecd6317fdd4de47f76a98eb18abaf371", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-repo-slugs-github.reposlug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-repo-slugs-github.reposlug-1.json index 165b8e0ac2c..be2b0697498 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-repo-slugs-github.reposlug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-repo-slugs-github.reposlug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-repo-slugs", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c4272385ed3b0de4feab38de9e4f6363ecd6317fdd4de47f76a98eb18abaf371", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.addissuecommentbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.addissuecommentbyslug-1.json index f776213bf2c..04dafc45c2e 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.addissuecommentbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.addissuecommentbyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-comments-issue", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuebyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuebyslug-1.json index 5d4cb5623ad..2a3fcf77a47 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuebyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuebyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-comments-issue", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuecommentbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuecommentbyslug-1.json index 7084b8b607f..b4431143e5c 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuecommentbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuecommentbyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-comments-issue", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-pr-github.project.updatepullrequestbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-pr-github.project.updatepullrequestbyslug-1.json index 1570e40effa..d71226e7392 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-pr-github.project.updatepullrequestbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-pr-github.project.updatepullrequestbyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-comments-pr", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-detail-github.project.workitemdetailsbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-detail-github.project.workitemdetailsbyslug-1.json index 1389d17f632..153999c8fa3 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-detail-github.project.workitemdetailsbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-detail-github.project.workitemdetailsbyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-detail", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f8f6e5d500f959b9b15c5498885a05422747880b6aef4ad795bc3064ebbacea6", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.clearitemfield-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.clearitemfield-1.json index f51ad0b39c8..670fbf334b6 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.clearitemfield-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.clearitemfield-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-fields", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateissuetypebyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateissuetypebyslug-1.json index 25cbb86afb6..e9e8591fb4b 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateissuetypebyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateissuetypebyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-fields", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateitemfield-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateitemfield-1.json index 598b31e2c35..f2cde8c7876 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateitemfield-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateitemfield-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-fields", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.addprreviewcomment-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.addprreviewcomment-1.json index 8a5d618b70e..68f7c60b216 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.addprreviewcomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.addprreviewcomment-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-files-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b228732762828412ad3d9eec3ece00a897d866046e37044322c3911758d6e0a9", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.mergepr-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.mergepr-1.json index b1ce1595bdd..93de2be604a 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.mergepr-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.mergepr-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-files-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b228732762828412ad3d9eec3ece00a897d866046e37044322c3911758d6e0a9", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.prfilecontents-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.prfilecontents-1.json index 6e0bdcbb4bb..0a95358d600 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.prfilecontents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.prfilecontents-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-files-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b228732762828412ad3d9eec3ece00a897d866046e37044322c3911758d6e0a9", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateissue-1.json index 9bd02baa925..295175d0694 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-files-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b228732762828412ad3d9eec3ece00a897d866046e37044322c3911758d6e0a9", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateprstate-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateprstate-1.json index 126b4d22b89..d3c1183ceef 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateprstate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateprstate-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-files-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b228732762828412ad3d9eec3ece00a897d866046e37044322c3911758d6e0a9", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listassignableusersbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listassignableusersbyslug-1.json index 31483b680e0..3912c126137 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listassignableusersbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listassignableusersbyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-metadata-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f8f6e5d500f959b9b15c5498885a05422747880b6aef4ad795bc3064ebbacea6", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listissuetypesbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listissuetypesbyslug-1.json index 7152a57420b..60bf2d3acc4 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listissuetypesbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listissuetypesbyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-metadata-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f8f6e5d500f959b9b15c5498885a05422747880b6aef4ad795bc3064ebbacea6", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listlabelsbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listlabelsbyslug-1.json index b873a7a9270..ebd38177b39 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listlabelsbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listlabelsbyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-metadata-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f8f6e5d500f959b9b15c5498885a05422747880b6aef4ad795bc3064ebbacea6", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.prchecks-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.prchecks-1.json index 7cf7224b191..aafb025f855 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.prchecks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.prchecks-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-review-checks", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.requestprreviewers-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.requestprreviewers-1.json index 938a223e4c4..b9be225d833 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.requestprreviewers-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.requestprreviewers-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-review-checks", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.rerunprchecks-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.rerunprchecks-1.json index cf6f8b13a09..d98a5dd9112 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.rerunprchecks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.rerunprchecks-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-review-checks", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.setprfileviewed-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.setprfileviewed-1.json index d1caf976991..935ae8eee83 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.setprfileviewed-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.setprfileviewed-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-review-checks", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addissuecomment-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addissuecomment-1.json index 96eaf154133..7b6a0350757 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addissuecomment-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addissuecomment-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-threads", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addprreviewcommentreply-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addprreviewcommentreply-1.json index 68c2697ee2f..f91b2bb82a6 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addprreviewcommentreply-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addprreviewcommentreply-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-threads", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.project.deleteissuecommentbyslug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.project.deleteissuecommentbyslug-1.json index 8c4339876d6..2c027586e2c 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.project.deleteissuecommentbyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.project.deleteissuecommentbyslug-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-threads", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.resolvereviewthread-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.resolvereviewthread-1.json index 8c37970c562..ed692389850 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.resolvereviewthread-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.resolvereviewthread-1.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-threads", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.countworkitems-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.countworkitems-1.json index fe1ee8c81e5..9efb85efffd 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.countworkitems-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.countworkitems-1.json @@ -3,7 +3,7 @@ "family": "tasks.provider-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.listworkitems-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.listworkitems-1.json index db67083f530..4623c580bb8 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.listworkitems-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.listworkitems-1.json @@ -3,7 +3,7 @@ "family": "tasks.provider-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.listteams-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.listteams-1.json index 0560b7a88d9..df9d28e0f9b 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.listteams-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.listteams-1.json @@ -3,7 +3,7 @@ "family": "tasks.provider-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.status-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.status-1.json index ec5129a1e4d..06b12c2163f 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.status-1.json @@ -3,7 +3,7 @@ "family": "tasks.provider-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-settings.update-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-settings.update-1.json index 54f2ca95fdc..7bdd0059243 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-settings.update-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.provider-load-settings.update-1.json @@ -3,7 +3,7 @@ "family": "tasks.provider-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.route-repo-list-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.route-repo-list-repo.list-1.json index 14d2f60761e..30b6ad18272 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.route-repo-list-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.route-repo-list-repo.list-1.json @@ -3,7 +3,7 @@ "family": "tasks.route-repo-list", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "feb6cee1ab7ecff1ba98bfba22d4924c748d3bb6b749db460cb617ee50b92f2c", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json index 4a509290d2e..bb1f713f98c 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json @@ -3,7 +3,7 @@ "family": "tasks.smart-source-search", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json index 72ef7fd00d4..15af2f98ce0 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json @@ -3,7 +3,7 @@ "family": "tasks.smart-source-search", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json index 380e6b68c36..0b1bd8d5eaa 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json @@ -3,7 +3,7 @@ "family": "tasks.smart-source-search", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json index 83278ef0931..ddfbeb459c5 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json @@ -3,7 +3,7 @@ "family": "tasks.smart-source-search", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json index f268bd20f2b..6df34401109 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json @@ -3,7 +3,7 @@ "family": "tasks.smart-source-search", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-github.createissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-github.createissue-1.json index cc193fea1f5..09a3cd9056c 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-github.createissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-github.createissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.task-create-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-repo.update-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-repo.update-1.json index 2239020ada0..8c3f5770e6b 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-repo.update-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-repo.update-1.json @@ -3,7 +3,7 @@ "family": "tasks.task-create-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.task-create-gitlab-gitlab.createissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.task-create-gitlab-gitlab.createissue-1.json index 944a0549a6c..0c257530c36 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.task-create-gitlab-gitlab.createissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.task-create-gitlab-gitlab.createissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.task-create-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.task-create-linear-linear.createissue-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.task-create-linear-linear.createissue-1.json index f6239258356..c11d58ddddd 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.task-create-linear-linear.createissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.task-create-linear-linear.createissue-1.json @@ -3,7 +3,7 @@ "family": "tasks.task-create-linear", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-items-gitlab.listworkitems-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-items-gitlab.listworkitems-1.json index 3dba064bb1c..9dc4d447e03 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-items-gitlab.listworkitems-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-items-gitlab.listworkitems-1.json @@ -3,7 +3,7 @@ "family": "tasks.task-list-gitlab-items", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-todos-gitlab.todos-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-todos-gitlab.todos-1.json index 79b8c640b7f..db48d87ee35 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-todos-gitlab.todos-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-todos-gitlab.todos-1.json @@ -3,7 +3,7 @@ "family": "tasks.task-list-gitlab-todos", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.listissues-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.listissues-1.json index 8333ce3087b..293c629d0cc 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.listissues-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.listissues-1.json @@ -3,7 +3,7 @@ "family": "tasks.task-list-linear", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.searchissues-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.searchissues-1.json index b1269ed0dc8..43b521f9190 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.searchissues-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.searchissues-1.json @@ -3,7 +3,7 @@ "family": "tasks.task-list-linear", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json index 0697b535d5f..65fe3e75195 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-source", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json index 5e24692439d..436c99c9db9 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-source", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json index dcc6a45bef0..a900f6a226e 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-sparse", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json index 79fed1caff1..5106704d7af 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-sparse", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json index 8342b88f71e..6c6c50622a0 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-ssh-local", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json index 208942fe11f..8abb1282d7c 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-ssh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json index c50d43b820a..5e210ecbc2c 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-ssh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json index ad89f54f4c8..4e38810a165 100644 --- a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-ssh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/matrix-terminal.query-reply-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-terminal.query-reply-terminal.send-1.json index 6b1547b83a8..8ca0604a783 100644 --- a/mobile/rpc-foundation/goldens/matrix-terminal.query-reply-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-terminal.query-reply-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "terminal.query-reply", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/matrix-terminal.raw-input-orchestration.workerterminaluserinput-1.json b/mobile/rpc-foundation/goldens/matrix-terminal.raw-input-orchestration.workerterminaluserinput-1.json index 000afa2806b..9dd92a3633d 100644 --- a/mobile/rpc-foundation/goldens/matrix-terminal.raw-input-orchestration.workerterminaluserinput-1.json +++ b/mobile/rpc-foundation/goldens/matrix-terminal.raw-input-orchestration.workerterminaluserinput-1.json @@ -3,7 +3,7 @@ "family": "terminal.raw-input", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/matrix-terminal.raw-input-terminal.send-1.json b/mobile/rpc-foundation/goldens/matrix-terminal.raw-input-terminal.send-1.json index d7b90cb740d..380472cd0d8 100644 --- a/mobile/rpc-foundation/goldens/matrix-terminal.raw-input-terminal.send-1.json +++ b/mobile/rpc-foundation/goldens/matrix-terminal.raw-input-terminal.send-1.json @@ -3,7 +3,7 @@ "family": "terminal.raw-input", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-1.json b/mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-1.json index f98eb119785..3e239fa6b68 100644 --- a/mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-1.json +++ b/mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-1.json @@ -3,7 +3,7 @@ "family": "terminal.takeover-report", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-2.json b/mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-2.json index 0c3dd0f6a07..bc489eca7de 100644 --- a/mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-2.json +++ b/mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-2.json @@ -3,7 +3,7 @@ "family": "terminal.takeover-report", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/matrix-terminal.viewport-refit-terminal.updateviewport-1.json b/mobile/rpc-foundation/goldens/matrix-terminal.viewport-refit-terminal.updateviewport-1.json index db6e4063096..c6b765448c2 100644 --- a/mobile/rpc-foundation/goldens/matrix-terminal.viewport-refit-terminal.updateviewport-1.json +++ b/mobile/rpc-foundation/goldens/matrix-terminal.viewport-refit-terminal.updateviewport-1.json @@ -3,7 +3,7 @@ "family": "terminal.viewport-refit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/matrix-transport.capability-probe-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-transport.capability-probe-status.get-1.json index f6801709469..dcb62bbe991 100644 --- a/mobile/rpc-foundation/goldens/matrix-transport.capability-probe-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-transport.capability-probe-status.get-1.json @@ -3,7 +3,7 @@ "family": "transport.capability-probe", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/matrix-transport.host-status-gates-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-transport.host-status-gates-status.get-1.json index f0b59591ffc..eda010626d0 100644 --- a/mobile/rpc-foundation/goldens/matrix-transport.host-status-gates-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-transport.host-status-gates-status.get-1.json @@ -3,7 +3,7 @@ "family": "transport.host-status-gates", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/matrix-transport.pairing-race-direct-status.json b/mobile/rpc-foundation/goldens/matrix-transport.pairing-race-direct-status.json index 4788382dfe1..608b3649bd9 100644 --- a/mobile/rpc-foundation/goldens/matrix-transport.pairing-race-direct-status.json +++ b/mobile/rpc-foundation/goldens/matrix-transport.pairing-race-direct-status.json @@ -3,7 +3,7 @@ "family": "transport.pairing-race", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/matrix-transport.pairing-race-relay-status.json b/mobile/rpc-foundation/goldens/matrix-transport.pairing-race-relay-status.json index e18ba900051..a8102b51879 100644 --- a/mobile/rpc-foundation/goldens/matrix-transport.pairing-race-relay-status.json +++ b/mobile/rpc-foundation/goldens/matrix-transport.pairing-race-relay-status.json @@ -3,7 +3,7 @@ "family": "transport.pairing-race", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.agent-launch-create-agent.launch-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.agent-launch-create-agent.launch-1.json index 547db031a77..e78faed10b4 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.agent-launch-create-agent.launch-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.agent-launch-create-agent.launch-1.json @@ -3,7 +3,7 @@ "family": "worktree.agent-launch-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json index 74dcffc35a1..f3ed566d096 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json @@ -3,7 +3,7 @@ "family": "worktree.catalog-snapshot", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json index 2a1cbabab1a..1556bd80d2a 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json @@ -3,7 +3,7 @@ "family": "worktree.create-retry", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json index cb0b0624633..4575f60e48d 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json @@ -3,7 +3,7 @@ "family": "worktree.home-catalog", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json index 1f51bc8ee3e..822399e247f 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json @@ -3,7 +3,7 @@ "family": "worktree.hosted-base", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json index d7453f577ac..11edd88340b 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json @@ -3,7 +3,7 @@ "family": "worktree.hosted-base", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json index baaa2b016f4..72e04ff0a32 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json @@ -3,7 +3,7 @@ "family": "worktree.retired-names", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json index 5bf1ae4bf2f..507dcf8bd9e 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json @@ -3,7 +3,7 @@ "family": "worktree.review-link", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json index 6d99ad29779..76cc9013278 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json @@ -3,7 +3,7 @@ "family": "worktree.runtime-capabilities", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json index ba6d6503b54..8a69535a78c 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json @@ -3,7 +3,7 @@ "family": "worktree.setup-hook-trust", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/mobile-web-bundle-build-changed.json b/mobile/rpc-foundation/goldens/mobile-web-bundle-build-changed.json new file mode 100644 index 00000000000..27422acce04 --- /dev/null +++ b/mobile/rpc-foundation/goldens/mobile-web-bundle-build-changed.json @@ -0,0 +1,210 @@ +{ + "operation": "mobileWeb.bundle-fetch", + "family": "mobileWeb.bundle-fetch", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "cef01677c0032d012985aa1be847dbe180f1df9aadd2d13f897c23dee1066d1a", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "081e772e5b64": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "invalid_argument", + "message": "mobile_web_bundle_build_changed" + }, + "id": "frame-3", + "ok": false + } + } + }, + "23871e324a00": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + } + }, + "2988ade7daff": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 6, + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":0}}" + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "782d48c615ad": { + "name": "bundle-progress", + "ordinal": 7, + "value": { + "completedAssets": 1, + "receivedBytes": 11, + "totalAssets": 2 + } + }, + "79725d734e7d": { + "assets": { + "$rpc": "null" + }, + "outcome": "refused: mobile_web_bundle_build_changed" + }, + "82b2a7971876": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "invalid_argument: mobile_web_bundle_build_changed", + "isRpcDeliveryUnknown": false + } + }, + "a0c050b31ee2": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"assets/app.js\",\"offset\":0}}" + }, + "ae7f1100e6e5": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "assetByteLength": 11, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "b3JjYS5ib290KCk=", + "eof": true, + "offset": 0, + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + } + } + } + } + }, + "recording": { + "scenario": "mobile-web-bundle-build-changed", + "checkpoints": [ + { + "id": "bundle-build-changed", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "081e772e5b64"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff"], + "settlements": { + "fetch": "82b2a7971876" + }, + "state": "79725d734e7d", + "effects": ["782d48c615ad"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json b/mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json new file mode 100644 index 00000000000..d6f0d35e6b4 --- /dev/null +++ b/mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json @@ -0,0 +1,276 @@ +{ + "operation": "mobileWeb.bundle-fetch", + "family": "mobileWeb.bundle-fetch", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "cec3f6a9dd09a2527f150626d15e3040b741bbf83a823f1656e0f3449c6ed80e", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "23871e324a00": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + } + }, + "2988ade7daff": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 6, + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":0}}" + }, + "2bfa7c81f55f": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 9, + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"index.html\",\"offset\":16}}" + }, + "2ff2addcb1f6": { + "name": "bundle-progress", + "ordinal": 10, + "value": { + "completedAssets": 2, + "receivedBytes": 37, + "totalAssets": 2 + } + }, + "573943fdb37d": { + "assets": { + "assets/app.js": "orca.boot()", + "index.html": "

orca

" + }, + "outcome": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "76ca82e57d04": { + "name": "mobileWeb.bundle.chunk#2", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "PCFkb2N0eXBlIGh0bWw+PA==", + "eof": false, + "offset": 0, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "782d48c615ad": { + "name": "bundle-progress", + "ordinal": 7, + "value": { + "completedAssets": 1, + "receivedBytes": 11, + "totalAssets": 2 + } + }, + "a0c050b31ee2": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.chunk\",\"params\":{\"buildId\":\"973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177\",\"path\":\"assets/app.js\",\"offset\":0}}" + }, + "ae7f1100e6e5": { + "name": "mobileWeb.bundle.chunk#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 0, + "path": "assets/app.js" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "assetByteLength": 11, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "b3JjYS5ib290KCk=", + "eof": true, + "offset": 0, + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + } + } + } + }, + "c319ae866f13": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "assetCount": 2, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "totalBytes": 37 + } + }, + "ce7abd9f93bb": { + "name": "mobileWeb.bundle.chunk#3", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.chunk" + }, + { + "name": "params", + "value": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "offset": 16, + "path": "index.html" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "assetByteLength": 26, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "dataBase64": "cD5vcmNhPC9wPg==", + "eof": true, + "offset": 16, + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + } + } + } + }, + "recording": { + "scenario": "mobile-web-bundle-fetch-paged", + "checkpoints": [ + { + "id": "bundle-fetched", + "observation": { + "sender": ["23871e324a00", "ae7f1100e6e5", "76ca82e57d04", "ce7abd9f93bb"], + "payloads": ["775bfa42070b", "a0c050b31ee2", "2988ade7daff", "2bfa7c81f55f"], + "settlements": { + "fetch": "c319ae866f13" + }, + "state": "573943fdb37d", + "effects": ["782d48c615ad", "2ff2addcb1f6"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json b/mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json new file mode 100644 index 00000000000..956a5e9c8a7 --- /dev/null +++ b/mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json @@ -0,0 +1,135 @@ +{ + "operation": "mobileWeb.bundle-manifest", + "family": "mobileWeb.bundle-manifest", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "5e9f4e6878be3f534288a90f7ccabc9cfe4e968aa29da372a98c3064d8e4d89e", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "23871e324a00": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + } + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "9ca9b2d0fc07": { + "outcome": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "chunkBytes": 16, + "entrypoint": "index.html", + "paths": ["assets/app.js", "index.html"] + } + }, + "9d87b3b6bd74": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "chunkBytes": 16, + "manifest": { + "assets": [ + { + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8", + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d" + }, + { + "byteLength": 26, + "contentType": "text/html; charset=utf-8", + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e" + } + ], + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "entrypoint": "index.html", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "schemaVersion": 1, + "totalBytes": 37 + } + } + } + }, + "recording": { + "scenario": "mobile-web-bundle-manifest-read", + "checkpoints": [ + { + "id": "manifest-read", + "observation": { + "sender": ["23871e324a00"], + "payloads": ["775bfa42070b"], + "settlements": { + "read": "9d87b3b6bd74" + }, + "state": "9ca9b2d0fc07", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json b/mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json new file mode 100644 index 00000000000..b1401dfc86c --- /dev/null +++ b/mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json @@ -0,0 +1,90 @@ +{ + "operation": "mobileWeb.bundle-fetch", + "family": "mobileWeb.bundle-fetch", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", + "scenarioSha256": "7333b1c83120bcfd9bdac8888ec1528c1db9b36325509f1328a551676df219c5", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "6858fdcae414": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "invalid_argument: mobile_web_bundle_unavailable", + "isRpcDeliveryUnknown": false + } + }, + "775bfa42070b": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"mobileWeb.bundle.manifest\",\"params\":null}" + }, + "8ea0c6a90560": { + "assets": { + "$rpc": "null" + }, + "outcome": "refused: mobile_web_bundle_unavailable" + }, + "fe8605eb5d5d": { + "name": "mobileWeb.bundle.manifest#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "mobileWeb.bundle.manifest" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "undefined" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "invalid_argument", + "message": "mobile_web_bundle_unavailable" + }, + "id": "frame-1", + "ok": false + } + } + } + }, + "recording": { + "scenario": "mobile-web-bundle-unavailable", + "checkpoints": [ + { + "id": "bundle-unavailable", + "observation": { + "sender": ["fe8605eb5d5d"], + "payloads": ["775bfa42070b"], + "settlements": { + "fetch": "6858fdcae414" + }, + "state": "8ea0c6a90560", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/native-chat-image-paste-single.json b/mobile/rpc-foundation/goldens/native-chat-image-paste-single.json index d57d8a35296..34b7a3cde17 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-paste-single.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-paste-single.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-image-paste-stops-on-rejection.json b/mobile/rpc-foundation/goldens/native-chat-image-paste-stops-on-rejection.json index e3f85ba2b40..69848989291 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-paste-stops-on-rejection.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-paste-stops-on-rejection.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-image-paste-trailing-image.json b/mobile/rpc-foundation/goldens/native-chat-image-paste-trailing-image.json index 75ec05f3ba0..c7c9110bc84 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-paste-trailing-image.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-paste-trailing-image.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-image-paste-two-images.json b/mobile/rpc-foundation/goldens/native-chat-image-paste-two-images.json index b6b24213264..fad3cf9c12e 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-paste-two-images.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-paste-two-images.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-image-upload-cancelled.json b/mobile/rpc-foundation/goldens/native-chat-image-upload-cancelled.json index 68053fbf1e9..1265fd5ce64 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-upload-cancelled.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-upload-cancelled.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-image-upload-second-fails.json b/mobile/rpc-foundation/goldens/native-chat-image-upload-second-fails.json index da8ad44da02..33050390c03 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-upload-second-fails.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-upload-second-fails.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-image-upload-single.json b/mobile/rpc-foundation/goldens/native-chat-image-upload-single.json index 721c41f9d03..284e04c5b1f 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-upload-single.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-upload-single.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-image-upload-start-refused.json b/mobile/rpc-foundation/goldens/native-chat-image-upload-start-refused.json index e3304ffc1df..3c13d7e7a46 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-upload-start-refused.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-upload-start-refused.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-image-upload-two.json b/mobile/rpc-foundation/goldens/native-chat-image-upload-two.json index e5b8734a885..3e9c0116c49 100644 --- a/mobile/rpc-foundation/goldens/native-chat-image-upload-two.json +++ b/mobile/rpc-foundation/goldens/native-chat-image-upload-two.json @@ -3,7 +3,7 @@ "family": "nativeChat.image-upload", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "16779b663f4faec8cbccdb42efd7b568a2912845b161f8a4827754687eb4235c", diff --git a/mobile/rpc-foundation/goldens/native-chat-page-earlier.json b/mobile/rpc-foundation/goldens/native-chat-page-earlier.json index 101f89e087c..e449bf6319c 100644 --- a/mobile/rpc-foundation/goldens/native-chat-page-earlier.json +++ b/mobile/rpc-foundation/goldens/native-chat-page-earlier.json @@ -3,7 +3,7 @@ "family": "session.native-chat-page", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "60ce67f66134d6385aa7fb47f2135436a7b3ce0221e3b2514bea65e06dba5518", diff --git a/mobile/rpc-foundation/goldens/native-chat-readability-local-repo.json b/mobile/rpc-foundation/goldens/native-chat-readability-local-repo.json index 3684c7d18c2..039fa9c363e 100644 --- a/mobile/rpc-foundation/goldens/native-chat-readability-local-repo.json +++ b/mobile/rpc-foundation/goldens/native-chat-readability-local-repo.json @@ -3,7 +3,7 @@ "family": "session.native-chat-readability", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/native-chat-readability-refused.json b/mobile/rpc-foundation/goldens/native-chat-readability-refused.json index b3ddc056eed..87908902563 100644 --- a/mobile/rpc-foundation/goldens/native-chat-readability-refused.json +++ b/mobile/rpc-foundation/goldens/native-chat-readability-refused.json @@ -3,7 +3,7 @@ "family": "session.native-chat-readability", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/native-chat-readability-remote-repo.json b/mobile/rpc-foundation/goldens/native-chat-readability-remote-repo.json index 75b08605a0f..c1fa8c47da8 100644 --- a/mobile/rpc-foundation/goldens/native-chat-readability-remote-repo.json +++ b/mobile/rpc-foundation/goldens/native-chat-readability-remote-repo.json @@ -3,7 +3,7 @@ "family": "session.native-chat-readability", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/native-chat-session-option-pick-empty.json b/mobile/rpc-foundation/goldens/native-chat-session-option-pick-empty.json index a1533cf44d8..1d8ece2ebad 100644 --- a/mobile/rpc-foundation/goldens/native-chat-session-option-pick-empty.json +++ b/mobile/rpc-foundation/goldens/native-chat-session-option-pick-empty.json @@ -3,7 +3,7 @@ "family": "nativeChat.session-option-pick", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/native-chat-session-option-pick-refused.json b/mobile/rpc-foundation/goldens/native-chat-session-option-pick-refused.json index e8af4d033b5..14f75a8fbbb 100644 --- a/mobile/rpc-foundation/goldens/native-chat-session-option-pick-refused.json +++ b/mobile/rpc-foundation/goldens/native-chat-session-option-pick-refused.json @@ -3,7 +3,7 @@ "family": "nativeChat.session-option-pick", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/native-chat-session-option-pick-written.json b/mobile/rpc-foundation/goldens/native-chat-session-option-pick-written.json index 24cb2bbfb5b..724de3fe781 100644 --- a/mobile/rpc-foundation/goldens/native-chat-session-option-pick-written.json +++ b/mobile/rpc-foundation/goldens/native-chat-session-option-pick-written.json @@ -3,7 +3,7 @@ "family": "nativeChat.session-option-pick", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/native-chat-stop-accepted.json b/mobile/rpc-foundation/goldens/native-chat-stop-accepted.json index dd0474ef3ad..c679056c415 100644 --- a/mobile/rpc-foundation/goldens/native-chat-stop-accepted.json +++ b/mobile/rpc-foundation/goldens/native-chat-stop-accepted.json @@ -3,7 +3,7 @@ "family": "session.native-chat-stop", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/native-chat-stop-both-rejected.json b/mobile/rpc-foundation/goldens/native-chat-stop-both-rejected.json index c5035e941da..bb702bed6f4 100644 --- a/mobile/rpc-foundation/goldens/native-chat-stop-both-rejected.json +++ b/mobile/rpc-foundation/goldens/native-chat-stop-both-rejected.json @@ -3,7 +3,7 @@ "family": "session.native-chat-stop", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/native-chat-stop-delivery-unknown.json b/mobile/rpc-foundation/goldens/native-chat-stop-delivery-unknown.json index e84ce07ca3a..666f89b5879 100644 --- a/mobile/rpc-foundation/goldens/native-chat-stop-delivery-unknown.json +++ b/mobile/rpc-foundation/goldens/native-chat-stop-delivery-unknown.json @@ -3,7 +3,7 @@ "family": "session.native-chat-stop", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/native-chat-write-accepted.json b/mobile/rpc-foundation/goldens/native-chat-write-accepted.json index b5878d28cd1..b7caeca64b2 100644 --- a/mobile/rpc-foundation/goldens/native-chat-write-accepted.json +++ b/mobile/rpc-foundation/goldens/native-chat-write-accepted.json @@ -3,7 +3,7 @@ "family": "nativeChat.terminal-write", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/native-chat-write-clear-line.json b/mobile/rpc-foundation/goldens/native-chat-write-clear-line.json index 3c7e4e45d16..f3debdaeb2c 100644 --- a/mobile/rpc-foundation/goldens/native-chat-write-clear-line.json +++ b/mobile/rpc-foundation/goldens/native-chat-write-clear-line.json @@ -3,7 +3,7 @@ "family": "nativeChat.terminal-write", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/native-chat-write-delivery-unknown.json b/mobile/rpc-foundation/goldens/native-chat-write-delivery-unknown.json index 5c57d1ee792..2ee43c70bd8 100644 --- a/mobile/rpc-foundation/goldens/native-chat-write-delivery-unknown.json +++ b/mobile/rpc-foundation/goldens/native-chat-write-delivery-unknown.json @@ -3,7 +3,7 @@ "family": "nativeChat.terminal-write", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/native-chat-write-rejected.json b/mobile/rpc-foundation/goldens/native-chat-write-rejected.json index 105c374abc2..f16bf79f939 100644 --- a/mobile/rpc-foundation/goldens/native-chat-write-rejected.json +++ b/mobile/rpc-foundation/goldens/native-chat-write-rejected.json @@ -3,7 +3,7 @@ "family": "nativeChat.terminal-write", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/native-chat-write-typed-command.json b/mobile/rpc-foundation/goldens/native-chat-write-typed-command.json index 74998fe3cce..398a4e3206b 100644 --- a/mobile/rpc-foundation/goldens/native-chat-write-typed-command.json +++ b/mobile/rpc-foundation/goldens/native-chat-write-typed-command.json @@ -3,7 +3,7 @@ "family": "nativeChat.terminal-write", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b98a3ca3818678b88cf629bc4635300a103637126a136f025a1600dc16f08008", diff --git a/mobile/rpc-foundation/goldens/new-tab-local-agents.json b/mobile/rpc-foundation/goldens/new-tab-local-agents.json index 3bd6e796e72..7cad9cf79c3 100644 --- a/mobile/rpc-foundation/goldens/new-tab-local-agents.json +++ b/mobile/rpc-foundation/goldens/new-tab-local-agents.json @@ -3,7 +3,7 @@ "family": "settings.new-tab-local-agents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/new-workspace-repositories-fulfilled.json b/mobile/rpc-foundation/goldens/new-workspace-repositories-fulfilled.json index 7b82dbebb6c..77b47e280f7 100644 --- a/mobile/rpc-foundation/goldens/new-workspace-repositories-fulfilled.json +++ b/mobile/rpc-foundation/goldens/new-workspace-repositories-fulfilled.json @@ -3,7 +3,7 @@ "family": "components.new-workspace-repositories", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "64c1772f0f95a3c43fbb14398a8804b2b4784f7f18874e4fd79767ae634c7faa", diff --git a/mobile/rpc-foundation/goldens/notifications-desktop-stream-closed.json b/mobile/rpc-foundation/goldens/notifications-desktop-stream-closed.json index 340afbd799b..e4c4ae6304f 100644 --- a/mobile/rpc-foundation/goldens/notifications-desktop-stream-closed.json +++ b/mobile/rpc-foundation/goldens/notifications-desktop-stream-closed.json @@ -3,7 +3,7 @@ "family": "notifications.desktop-stream", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "eacf859143588ae6bee2804975d642b6c1088d57ae77ffe298620250d9a9f0e4", diff --git a/mobile/rpc-foundation/goldens/notifications-desktop-stream-replayed.json b/mobile/rpc-foundation/goldens/notifications-desktop-stream-replayed.json index 03067bafad6..bf1f6f0c525 100644 --- a/mobile/rpc-foundation/goldens/notifications-desktop-stream-replayed.json +++ b/mobile/rpc-foundation/goldens/notifications-desktop-stream-replayed.json @@ -3,7 +3,7 @@ "family": "notifications.desktop-stream", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "eacf859143588ae6bee2804975d642b6c1088d57ae77ffe298620250d9a9f0e4", diff --git a/mobile/rpc-foundation/goldens/notifications-desktop-stream.json b/mobile/rpc-foundation/goldens/notifications-desktop-stream.json index 3e9f6f6e105..e363a8790b4 100644 --- a/mobile/rpc-foundation/goldens/notifications-desktop-stream.json +++ b/mobile/rpc-foundation/goldens/notifications-desktop-stream.json @@ -3,7 +3,7 @@ "family": "notifications.desktop-stream", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "eacf859143588ae6bee2804975d642b6c1088d57ae77ffe298620250d9a9f0e4", diff --git a/mobile/rpc-foundation/goldens/notifications-display-test-accepted.json b/mobile/rpc-foundation/goldens/notifications-display-test-accepted.json index 4126a883af4..ae0a3deba66 100644 --- a/mobile/rpc-foundation/goldens/notifications-display-test-accepted.json +++ b/mobile/rpc-foundation/goldens/notifications-display-test-accepted.json @@ -3,7 +3,7 @@ "family": "notifications.display-test-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "12a04986290715d5db23a1eb5192c1138bb2d375d1b1dd37ba310ea89eb11566", diff --git a/mobile/rpc-foundation/goldens/notifications-display-test-not-registered.json b/mobile/rpc-foundation/goldens/notifications-display-test-not-registered.json index 0f860f17f7d..b39af2d6b3f 100644 --- a/mobile/rpc-foundation/goldens/notifications-display-test-not-registered.json +++ b/mobile/rpc-foundation/goldens/notifications-display-test-not-registered.json @@ -3,7 +3,7 @@ "family": "notifications.display-test-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "12a04986290715d5db23a1eb5192c1138bb2d375d1b1dd37ba310ea89eb11566", diff --git a/mobile/rpc-foundation/goldens/notifications-display-test-rate-limited.json b/mobile/rpc-foundation/goldens/notifications-display-test-rate-limited.json index d3a4ad64ce8..ce2e3ad34c1 100644 --- a/mobile/rpc-foundation/goldens/notifications-display-test-rate-limited.json +++ b/mobile/rpc-foundation/goldens/notifications-display-test-rate-limited.json @@ -3,7 +3,7 @@ "family": "notifications.display-test-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "12a04986290715d5db23a1eb5192c1138bb2d375d1b1dd37ba310ea89eb11566", diff --git a/mobile/rpc-foundation/goldens/notifications-display-test-unknown-reason.json b/mobile/rpc-foundation/goldens/notifications-display-test-unknown-reason.json index 1cb9fb00c63..964660efa3d 100644 --- a/mobile/rpc-foundation/goldens/notifications-display-test-unknown-reason.json +++ b/mobile/rpc-foundation/goldens/notifications-display-test-unknown-reason.json @@ -3,7 +3,7 @@ "family": "notifications.display-test-screen", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "12a04986290715d5db23a1eb5192c1138bb2d375d1b1dd37ba310ea89eb11566", diff --git a/mobile/rpc-foundation/goldens/notifications-push-gateway-rejected.json b/mobile/rpc-foundation/goldens/notifications-push-gateway-rejected.json index 7ce35fa060e..372503d08f4 100644 --- a/mobile/rpc-foundation/goldens/notifications-push-gateway-rejected.json +++ b/mobile/rpc-foundation/goldens/notifications-push-gateway-rejected.json @@ -3,7 +3,7 @@ "family": "notifications.push-registration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2e3d939dc162dbc5a38d8a7207111688204a825fd70348721917b3016e1c9470", diff --git a/mobile/rpc-foundation/goldens/notifications-push-registered.json b/mobile/rpc-foundation/goldens/notifications-push-registered.json index 7884d2d5fdd..fc9ce141ee1 100644 --- a/mobile/rpc-foundation/goldens/notifications-push-registered.json +++ b/mobile/rpc-foundation/goldens/notifications-push-registered.json @@ -3,7 +3,7 @@ "family": "notifications.push-registration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2e3d939dc162dbc5a38d8a7207111688204a825fd70348721917b3016e1c9470", diff --git a/mobile/rpc-foundation/goldens/pairing-pre-profile-direct-wins-and-provisions.json b/mobile/rpc-foundation/goldens/pairing-pre-profile-direct-wins-and-provisions.json index 2f9d92dcd9f..719bbb2c806 100644 --- a/mobile/rpc-foundation/goldens/pairing-pre-profile-direct-wins-and-provisions.json +++ b/mobile/rpc-foundation/goldens/pairing-pre-profile-direct-wins-and-provisions.json @@ -3,7 +3,7 @@ "family": "pairing.pre-profile", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/pairing-pre-profile-provision-unsupported-saves-direct-host.json b/mobile/rpc-foundation/goldens/pairing-pre-profile-provision-unsupported-saves-direct-host.json index e8233126f72..20fcdbcf0bb 100644 --- a/mobile/rpc-foundation/goldens/pairing-pre-profile-provision-unsupported-saves-direct-host.json +++ b/mobile/rpc-foundation/goldens/pairing-pre-profile-provision-unsupported-saves-direct-host.json @@ -3,7 +3,7 @@ "family": "pairing.pre-profile", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/pairing-pre-profile-times-out.json b/mobile/rpc-foundation/goldens/pairing-pre-profile-times-out.json index bbbeef88765..ce47a62911a 100644 --- a/mobile/rpc-foundation/goldens/pairing-pre-profile-times-out.json +++ b/mobile/rpc-foundation/goldens/pairing-pre-profile-times-out.json @@ -3,7 +3,7 @@ "family": "pairing.pre-profile", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/pr-branch-identity.json b/mobile/rpc-foundation/goldens/pr-branch-identity.json index ca79995884f..8cbe1323348 100644 --- a/mobile/rpc-foundation/goldens/pr-branch-identity.json +++ b/mobile/rpc-foundation/goldens/pr-branch-identity.json @@ -3,7 +3,7 @@ "family": "session.pr-branch-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-branch-repo-context.json b/mobile/rpc-foundation/goldens/pr-branch-repo-context.json index 86b350a2faa..81056ec0c87 100644 --- a/mobile/rpc-foundation/goldens/pr-branch-repo-context.json +++ b/mobile/rpc-foundation/goldens/pr-branch-repo-context.json @@ -3,7 +3,7 @@ "family": "session.pr-branch-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-comment-mutation.json b/mobile/rpc-foundation/goldens/pr-comment-mutation.json index 3219211e1b7..7ae487db0c1 100644 --- a/mobile/rpc-foundation/goldens/pr-comment-mutation.json +++ b/mobile/rpc-foundation/goldens/pr-comment-mutation.json @@ -3,7 +3,7 @@ "family": "github.pr-comment-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-comment-resolve-unconfirmed.json b/mobile/rpc-foundation/goldens/pr-comment-resolve-unconfirmed.json index a714e866c5c..b91b86e8c63 100644 --- a/mobile/rpc-foundation/goldens/pr-comment-resolve-unconfirmed.json +++ b/mobile/rpc-foundation/goldens/pr-comment-resolve-unconfirmed.json @@ -3,7 +3,7 @@ "family": "github.pr-comment-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-mutation-in-band-failure.json b/mobile/rpc-foundation/goldens/pr-mutation-in-band-failure.json index eb1f87a9ebf..096dbf4bad6 100644 --- a/mobile/rpc-foundation/goldens/pr-mutation-in-band-failure.json +++ b/mobile/rpc-foundation/goldens/pr-mutation-in-band-failure.json @@ -3,7 +3,7 @@ "family": "github.pr-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-mutation-status.json b/mobile/rpc-foundation/goldens/pr-mutation-status.json index 0cd01c52126..0eeb24aeb2c 100644 --- a/mobile/rpc-foundation/goldens/pr-mutation-status.json +++ b/mobile/rpc-foundation/goldens/pr-mutation-status.json @@ -3,7 +3,7 @@ "family": "github.pr-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-read-fork-routing.json b/mobile/rpc-foundation/goldens/pr-read-fork-routing.json index 0313c5fc917..b45fa26e9a6 100644 --- a/mobile/rpc-foundation/goldens/pr-read-fork-routing.json +++ b/mobile/rpc-foundation/goldens/pr-read-fork-routing.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-read-surface.json b/mobile/rpc-foundation/goldens/pr-read-surface.json index ebe53958ae6..43bf9c1bf2b 100644 --- a/mobile/rpc-foundation/goldens/pr-read-surface.json +++ b/mobile/rpc-foundation/goldens/pr-read-surface.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-read-upstream-error.json b/mobile/rpc-foundation/goldens/pr-read-upstream-error.json index 1b7c56235a0..965bcc25200 100644 --- a/mobile/rpc-foundation/goldens/pr-read-upstream-error.json +++ b/mobile/rpc-foundation/goldens/pr-read-upstream-error.json @@ -3,7 +3,7 @@ "family": "github.pr-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-sidebar-checks-refused.json b/mobile/rpc-foundation/goldens/pr-sidebar-checks-refused.json index 5daa0cf3ad6..e50a91fbe0c 100644 --- a/mobile/rpc-foundation/goldens/pr-sidebar-checks-refused.json +++ b/mobile/rpc-foundation/goldens/pr-sidebar-checks-refused.json @@ -3,7 +3,7 @@ "family": "session.pr-sidebar", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/pr-sidebar-load.json b/mobile/rpc-foundation/goldens/pr-sidebar-load.json index 426056b00bf..d22b3326902 100644 --- a/mobile/rpc-foundation/goldens/pr-sidebar-load.json +++ b/mobile/rpc-foundation/goldens/pr-sidebar-load.json @@ -3,7 +3,7 @@ "family": "session.pr-sidebar", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/pr-title-mutation.json b/mobile/rpc-foundation/goldens/pr-title-mutation.json index 5e1e60edfdf..26e35d30722 100644 --- a/mobile/rpc-foundation/goldens/pr-title-mutation.json +++ b/mobile/rpc-foundation/goldens/pr-title-mutation.json @@ -3,7 +3,7 @@ "family": "github.pr-title-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-title-unconfirmed.json b/mobile/rpc-foundation/goldens/pr-title-unconfirmed.json index fdb63408d7c..310b7b63410 100644 --- a/mobile/rpc-foundation/goldens/pr-title-unconfirmed.json +++ b/mobile/rpc-foundation/goldens/pr-title-unconfirmed.json @@ -3,7 +3,7 @@ "family": "github.pr-title-mutation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-triage-invalid-terminal.json b/mobile/rpc-foundation/goldens/pr-triage-invalid-terminal.json index 0e04e71edf3..6cd9f8fc76b 100644 --- a/mobile/rpc-foundation/goldens/pr-triage-invalid-terminal.json +++ b/mobile/rpc-foundation/goldens/pr-triage-invalid-terminal.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-triage-launch.json b/mobile/rpc-foundation/goldens/pr-triage-launch.json index 2b83758fb98..5cf8796ee6c 100644 --- a/mobile/rpc-foundation/goldens/pr-triage-launch.json +++ b/mobile/rpc-foundation/goldens/pr-triage-launch.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/pr-triage-send-locked.json b/mobile/rpc-foundation/goldens/pr-triage-send-locked.json index 1c86d532927..0cd444dc8a8 100644 --- a/mobile/rpc-foundation/goldens/pr-triage-send-locked.json +++ b/mobile/rpc-foundation/goldens/pr-triage-send-locked.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f560ff1582487c118756c527faa1fb1f65e37ce22ca9573491fd4b2dbcedf38d", diff --git a/mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json b/mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json index 65482972dcf..82377a1aaf0 100644 --- a/mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json +++ b/mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json b/mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json index 19d5ff71b7a..46a5f4a8c13 100644 --- a/mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json +++ b/mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json b/mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json index 640cd50b1fb..738a0c210b8 100644 --- a/mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json +++ b/mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json b/mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json index f8ce7ad235e..736ad95562a 100644 --- a/mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json +++ b/mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/push-dismissal-tray-reconciled.json b/mobile/rpc-foundation/goldens/push-dismissal-tray-reconciled.json index 0c80acc02b8..808aacb3831 100644 --- a/mobile/rpc-foundation/goldens/push-dismissal-tray-reconciled.json +++ b/mobile/rpc-foundation/goldens/push-dismissal-tray-reconciled.json @@ -3,7 +3,7 @@ "family": "notifications.push-dismissal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "595a3eb2994d0596b9fcd0707b175b4e978625053dfbc5c541b0350c0cbfb524", diff --git a/mobile/rpc-foundation/goldens/quick-commands-load-refused.json b/mobile/rpc-foundation/goldens/quick-commands-load-refused.json index 4ac464e3289..d3450316af3 100644 --- a/mobile/rpc-foundation/goldens/quick-commands-load-refused.json +++ b/mobile/rpc-foundation/goldens/quick-commands-load-refused.json @@ -3,7 +3,7 @@ "family": "settings.quick-commands", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/quick-commands-loaded-and-saved.json b/mobile/rpc-foundation/goldens/quick-commands-loaded-and-saved.json index 806471d0b2f..37889fad0dd 100644 --- a/mobile/rpc-foundation/goldens/quick-commands-loaded-and-saved.json +++ b/mobile/rpc-foundation/goldens/quick-commands-loaded-and-saved.json @@ -3,7 +3,7 @@ "family": "settings.quick-commands", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/quick-commands-save-refused-rolls-back.json b/mobile/rpc-foundation/goldens/quick-commands-save-refused-rolls-back.json index 857859ca472..bb270dfdf4f 100644 --- a/mobile/rpc-foundation/goldens/quick-commands-save-refused-rolls-back.json +++ b/mobile/rpc-foundation/goldens/quick-commands-save-refused-rolls-back.json @@ -3,7 +3,7 @@ "family": "settings.quick-commands", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/relay-direct-upgrade-commits.json b/mobile/rpc-foundation/goldens/relay-direct-upgrade-commits.json index a81f114b7d1..e5f09bb3c85 100644 --- a/mobile/rpc-foundation/goldens/relay-direct-upgrade-commits.json +++ b/mobile/rpc-foundation/goldens/relay-direct-upgrade-commits.json @@ -3,7 +3,7 @@ "family": "relay.direct-upgrade", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/relay-direct-upgrade-unsupported-host-declines.json b/mobile/rpc-foundation/goldens/relay-direct-upgrade-unsupported-host-declines.json index e03bd56fcc0..22805bddef6 100644 --- a/mobile/rpc-foundation/goldens/relay-direct-upgrade-unsupported-host-declines.json +++ b/mobile/rpc-foundation/goldens/relay-direct-upgrade-unsupported-host-declines.json @@ -3,7 +3,7 @@ "family": "relay.direct-upgrade", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/relay-pairing-recovery-invite-authorizes.json b/mobile/rpc-foundation/goldens/relay-pairing-recovery-invite-authorizes.json index fb6d02ff8fc..388d1e5a313 100644 --- a/mobile/rpc-foundation/goldens/relay-pairing-recovery-invite-authorizes.json +++ b/mobile/rpc-foundation/goldens/relay-pairing-recovery-invite-authorizes.json @@ -3,7 +3,7 @@ "family": "relay.pairing-recovery", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/relay-pairing-recovery-resume-committed.json b/mobile/rpc-foundation/goldens/relay-pairing-recovery-resume-committed.json index bc8c28434b9..003fd578314 100644 --- a/mobile/rpc-foundation/goldens/relay-pairing-recovery-resume-committed.json +++ b/mobile/rpc-foundation/goldens/relay-pairing-recovery-resume-committed.json @@ -3,7 +3,7 @@ "family": "relay.pairing-recovery", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e33d584229530c716ecdc44d198b95fcfb4dfd9468fba7d5222ee3f122950197", diff --git a/mobile/rpc-foundation/goldens/relay-rotation-installs-and-commits.json b/mobile/rpc-foundation/goldens/relay-rotation-installs-and-commits.json index bb5f2b3eabc..677042deaef 100644 --- a/mobile/rpc-foundation/goldens/relay-rotation-installs-and-commits.json +++ b/mobile/rpc-foundation/goldens/relay-rotation-installs-and-commits.json @@ -3,7 +3,7 @@ "family": "relay.credential-rotation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/relay-rotation-resumes-committed-pending.json b/mobile/rpc-foundation/goldens/relay-rotation-resumes-committed-pending.json index 3d8dd2b38fd..5d3f174cd90 100644 --- a/mobile/rpc-foundation/goldens/relay-rotation-resumes-committed-pending.json +++ b/mobile/rpc-foundation/goldens/relay-rotation-resumes-committed-pending.json @@ -3,7 +3,7 @@ "family": "relay.credential-rotation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "651e75383caf1b30c329dec2d5d4f0da5358c410402d03cbb087f39600d7a4d2", diff --git a/mobile/rpc-foundation/goldens/review-branch-diff-shapes.json b/mobile/rpc-foundation/goldens/review-branch-diff-shapes.json index a06c646de80..79f78d328b6 100644 --- a/mobile/rpc-foundation/goldens/review-branch-diff-shapes.json +++ b/mobile/rpc-foundation/goldens/review-branch-diff-shapes.json @@ -3,7 +3,7 @@ "family": "session.review-branch-diff", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/review-create-terminal-refused.json b/mobile/rpc-foundation/goldens/review-create-terminal-refused.json index 46828f345dd..158364c10f8 100644 --- a/mobile/rpc-foundation/goldens/review-create-terminal-refused.json +++ b/mobile/rpc-foundation/goldens/review-create-terminal-refused.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/review-file-diff-shapes.json b/mobile/rpc-foundation/goldens/review-file-diff-shapes.json index 65e52c63ec1..93fa7bd8af2 100644 --- a/mobile/rpc-foundation/goldens/review-file-diff-shapes.json +++ b/mobile/rpc-foundation/goldens/review-file-diff-shapes.json @@ -3,7 +3,7 @@ "family": "session.review-file-diff", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/review-git-mutations-run.json b/mobile/rpc-foundation/goldens/review-git-mutations-run.json index f5fb98b467d..b7d7028bc70 100644 --- a/mobile/rpc-foundation/goldens/review-git-mutations-run.json +++ b/mobile/rpc-foundation/goldens/review-git-mutations-run.json @@ -3,7 +3,7 @@ "family": "session.review-git-mutations", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/review-mark-reviewed-persists.json b/mobile/rpc-foundation/goldens/review-mark-reviewed-persists.json index 6a7746ef50e..512093321d8 100644 --- a/mobile/rpc-foundation/goldens/review-mark-reviewed-persists.json +++ b/mobile/rpc-foundation/goldens/review-mark-reviewed-persists.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/review-mark-reviewed-rolls-back.json b/mobile/rpc-foundation/goldens/review-mark-reviewed-rolls-back.json index ea34cc544bc..515a7cc72b4 100644 --- a/mobile/rpc-foundation/goldens/review-mark-reviewed-rolls-back.json +++ b/mobile/rpc-foundation/goldens/review-mark-reviewed-rolls-back.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/review-open-in-session.json b/mobile/rpc-foundation/goldens/review-open-in-session.json index 24b2de16d2c..1e760d2300c 100644 --- a/mobile/rpc-foundation/goldens/review-open-in-session.json +++ b/mobile/rpc-foundation/goldens/review-open-in-session.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/review-send-notes-heals-stale-input.json b/mobile/rpc-foundation/goldens/review-send-notes-heals-stale-input.json index 6913ec396fc..d0f820f0bd6 100644 --- a/mobile/rpc-foundation/goldens/review-send-notes-heals-stale-input.json +++ b/mobile/rpc-foundation/goldens/review-send-notes-heals-stale-input.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/review-send-sheet-lists-terminals.json b/mobile/rpc-foundation/goldens/review-send-sheet-lists-terminals.json index 49d03e26871..de968c3a13b 100644 --- a/mobile/rpc-foundation/goldens/review-send-sheet-lists-terminals.json +++ b/mobile/rpc-foundation/goldens/review-send-sheet-lists-terminals.json @@ -3,7 +3,7 @@ "family": "session.review-send-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/review-stage-file.json b/mobile/rpc-foundation/goldens/review-stage-file.json index 200db25467f..74868cca6f0 100644 --- a/mobile/rpc-foundation/goldens/review-stage-file.json +++ b/mobile/rpc-foundation/goldens/review-stage-file.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/review-stage-refused.json b/mobile/rpc-foundation/goldens/review-stage-refused.json index e6e51ecfb3c..a8680fcaf8f 100644 --- a/mobile/rpc-foundation/goldens/review-stage-refused.json +++ b/mobile/rpc-foundation/goldens/review-stage-refused.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "2d72b8e68a66a906394167beb8c78f1c0521ca1e731976fd26963ec3bfa9cca4", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-default.json b/mobile/rpc-foundation/goldens/sc-base-ref-default.json index 0bd5be32c3e..36189b62622 100644 --- a/mobile/rpc-foundation/goldens/sc-base-ref-default.json +++ b/mobile/rpc-foundation/goldens/sc-base-ref-default.json @@ -3,7 +3,7 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json b/mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json index 3e02f3c1945..72ef300b85d 100644 --- a/mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json +++ b/mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json @@ -3,7 +3,7 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json b/mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json index b4d848a5899..b9f121b5e68 100644 --- a/mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json +++ b/mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json @@ -3,7 +3,7 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json b/mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json index 334db434c62..907eb6e804b 100644 --- a/mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json +++ b/mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json @@ -3,7 +3,7 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-branch-diff-previewed.json b/mobile/rpc-foundation/goldens/sc-branch-diff-previewed.json index 231cc6c68c9..dcdbd7dba6e 100644 --- a/mobile/rpc-foundation/goldens/sc-branch-diff-previewed.json +++ b/mobile/rpc-foundation/goldens/sc-branch-diff-previewed.json @@ -3,7 +3,7 @@ "family": "git.branch-diff-preview", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/sc-changes-loaded.json b/mobile/rpc-foundation/goldens/sc-changes-loaded.json index 45330a9e030..66c03a1ce52 100644 --- a/mobile/rpc-foundation/goldens/sc-changes-loaded.json +++ b/mobile/rpc-foundation/goldens/sc-changes-loaded.json @@ -3,7 +3,7 @@ "family": "git.changes-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json b/mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json index 1d33974b501..c94829d8039 100644 --- a/mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json +++ b/mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json @@ -3,7 +3,7 @@ "family": "git.commit-message-ai", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-commit-message-canceled.json b/mobile/rpc-foundation/goldens/sc-commit-message-canceled.json index dcbd7111a9b..5c26f36f6b9 100644 --- a/mobile/rpc-foundation/goldens/sc-commit-message-canceled.json +++ b/mobile/rpc-foundation/goldens/sc-commit-message-canceled.json @@ -3,7 +3,7 @@ "family": "git.commit-message-ai", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-commit-message-generated.json b/mobile/rpc-foundation/goldens/sc-commit-message-generated.json index 5559ed69ffd..2a472aec29a 100644 --- a/mobile/rpc-foundation/goldens/sc-commit-message-generated.json +++ b/mobile/rpc-foundation/goldens/sc-commit-message-generated.json @@ -3,7 +3,7 @@ "family": "git.commit-message-ai", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-create-existing-review.json b/mobile/rpc-foundation/goldens/sc-create-existing-review.json index ba4bd0b58b3..01d6ffae0c3 100644 --- a/mobile/rpc-foundation/goldens/sc-create-existing-review.json +++ b/mobile/rpc-foundation/goldens/sc-create-existing-review.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json b/mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json index 190ab60c628..e9f44f57bfd 100644 --- a/mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json +++ b/mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-create-intent-unlisted-provider.json b/mobile/rpc-foundation/goldens/sc-create-intent-unlisted-provider.json index e829d9b8cf3..200c0c900fa 100644 --- a/mobile/rpc-foundation/goldens/sc-create-intent-unlisted-provider.json +++ b/mobile/rpc-foundation/goldens/sc-create-intent-unlisted-provider.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json b/mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json index 1f1815b7202..64205c7cf8a 100644 --- a/mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json +++ b/mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json b/mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json index 5e261656806..73ce0ca2035 100644 --- a/mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json +++ b/mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json b/mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json index 39f5350a457..9f8a45e5e04 100644 --- a/mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json +++ b/mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json b/mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json index bed68a8f440..c847b221df3 100644 --- a/mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json +++ b/mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json @@ -3,7 +3,7 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json b/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json index 6dfb82c4da8..d6a9f4bc894 100644 --- a/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json +++ b/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json @@ -3,7 +3,7 @@ "family": "hostedReview.eligibility", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-history-commit-files.json b/mobile/rpc-foundation/goldens/sc-history-commit-files.json index a6c296ae68d..397c405c07e 100644 --- a/mobile/rpc-foundation/goldens/sc-history-commit-files.json +++ b/mobile/rpc-foundation/goldens/sc-history-commit-files.json @@ -3,7 +3,7 @@ "family": "git.history-commit-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/sc-history-loaded.json b/mobile/rpc-foundation/goldens/sc-history-loaded.json index 688cb63da6f..e918b6aeadc 100644 --- a/mobile/rpc-foundation/goldens/sc-history-loaded.json +++ b/mobile/rpc-foundation/goldens/sc-history-loaded.json @@ -3,7 +3,7 @@ "family": "git.history-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json b/mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json index 34bd51514ae..221039ca4ba 100644 --- a/mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json +++ b/mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json @@ -3,7 +3,7 @@ "family": "worktree.review-link", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-pr-link-read.json b/mobile/rpc-foundation/goldens/sc-pr-link-read.json index 4b79af23456..0621f4938b9 100644 --- a/mobile/rpc-foundation/goldens/sc-pr-link-read.json +++ b/mobile/rpc-foundation/goldens/sc-pr-link-read.json @@ -3,7 +3,7 @@ "family": "worktree.review-link", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-pr-link-set.json b/mobile/rpc-foundation/goldens/sc-pr-link-set.json index 6245291f9b0..311cccfa9c2 100644 --- a/mobile/rpc-foundation/goldens/sc-pr-link-set.json +++ b/mobile/rpc-foundation/goldens/sc-pr-link-set.json @@ -3,7 +3,7 @@ "family": "worktree.review-link", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json b/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json index 5aed9372ed1..772a182ba82 100644 --- a/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json +++ b/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json @@ -3,7 +3,7 @@ "family": "hostedReview.eligibility", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json b/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json index c6ce63dba12..48b992d2d19 100644 --- a/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json +++ b/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json @@ -3,7 +3,7 @@ "family": "hostedReview.eligibility", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json b/mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json index 5ad745af476..8a04952f9bf 100644 --- a/mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json +++ b/mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json @@ -3,7 +3,7 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json b/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json index 0aa7ba9049a..8907e3e81c2 100644 --- a/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json +++ b/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json @@ -3,7 +3,7 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-push.json b/mobile/rpc-foundation/goldens/sc-prerequisite-push.json index 1fb00dbbe08..ec9d9073448 100644 --- a/mobile/rpc-foundation/goldens/sc-prerequisite-push.json +++ b/mobile/rpc-foundation/goldens/sc-prerequisite-push.json @@ -3,7 +3,7 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json b/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json index 8ce664bb345..6a11d6bfdf6 100644 --- a/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json +++ b/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json @@ -3,7 +3,7 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-reveal-first-poll.json b/mobile/rpc-foundation/goldens/sc-reveal-first-poll.json index 5f24e1f7000..1a5b3349f6f 100644 --- a/mobile/rpc-foundation/goldens/sc-reveal-first-poll.json +++ b/mobile/rpc-foundation/goldens/sc-reveal-first-poll.json @@ -3,7 +3,7 @@ "family": "session.tab-reveal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-reveal-timeout.json b/mobile/rpc-foundation/goldens/sc-reveal-timeout.json index 90a2b1bb083..47816ddc2a5 100644 --- a/mobile/rpc-foundation/goldens/sc-reveal-timeout.json +++ b/mobile/rpc-foundation/goldens/sc-reveal-timeout.json @@ -3,7 +3,7 @@ "family": "session.tab-reveal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json b/mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json index 7bd28f65347..dcb8cd11b55 100644 --- a/mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json +++ b/mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json @@ -3,7 +3,7 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json b/mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json index 5582621ee67..7d14be2af8e 100644 --- a/mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json +++ b/mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json @@ -3,7 +3,7 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-review-commit-rejected.json b/mobile/rpc-foundation/goldens/sc-review-commit-rejected.json index db321e4e48a..036416078dc 100644 --- a/mobile/rpc-foundation/goldens/sc-review-commit-rejected.json +++ b/mobile/rpc-foundation/goldens/sc-review-commit-rejected.json @@ -3,7 +3,7 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-review-commit.json b/mobile/rpc-foundation/goldens/sc-review-commit.json index d74c460731b..9e0a3d40baf 100644 --- a/mobile/rpc-foundation/goldens/sc-review-commit.json +++ b/mobile/rpc-foundation/goldens/sc-review-commit.json @@ -3,7 +3,7 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json b/mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json index 3f11935a97c..c465d93beff 100644 --- a/mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json +++ b/mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json @@ -3,7 +3,7 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-review-status-normalized.json b/mobile/rpc-foundation/goldens/sc-review-status-normalized.json index 51a267ca07b..b2a2f9bac19 100644 --- a/mobile/rpc-foundation/goldens/sc-review-status-normalized.json +++ b/mobile/rpc-foundation/goldens/sc-review-status-normalized.json @@ -3,7 +3,7 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/schedules-b3.json b/mobile/rpc-foundation/goldens/schedules-b3.json index b797ea3713d..c89136c1efc 100644 --- a/mobile/rpc-foundation/goldens/schedules-b3.json +++ b/mobile/rpc-foundation/goldens/schedules-b3.json @@ -3,7 +3,7 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json index e625f64c47a..b43ec58c4be 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json b/mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json index e5006d9a13d..eaa46f1e337 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json index d7fe2b57c81..f92b11bdccc 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json index 707b3b3912c..1d70e1b046c 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json index 042aa981983..d84fc521691 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json index 84978f6fe1f..50df5a1664f 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/session-browser-tab-created.json b/mobile/rpc-foundation/goldens/session-browser-tab-created.json index 62636b04ef0..a98478db1f1 100644 --- a/mobile/rpc-foundation/goldens/session-browser-tab-created.json +++ b/mobile/rpc-foundation/goldens/session-browser-tab-created.json @@ -3,7 +3,7 @@ "family": "session.browser-tab-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-create-browser-refused.json b/mobile/rpc-foundation/goldens/session-create-browser-refused.json index 55a72e1853a..a3a448cd82c 100644 --- a/mobile/rpc-foundation/goldens/session-create-browser-refused.json +++ b/mobile/rpc-foundation/goldens/session-create-browser-refused.json @@ -3,7 +3,7 @@ "family": "session.content-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-create-browser-tab.json b/mobile/rpc-foundation/goldens/session-create-browser-tab.json index c6fbdb84f80..04ecd4dc1b4 100644 --- a/mobile/rpc-foundation/goldens/session-create-browser-tab.json +++ b/mobile/rpc-foundation/goldens/session-create-browser-tab.json @@ -3,7 +3,7 @@ "family": "session.content-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-create-markdown-name-collision.json b/mobile/rpc-foundation/goldens/session-create-markdown-name-collision.json index 69ff1e45ce0..db01505f56f 100644 --- a/mobile/rpc-foundation/goldens/session-create-markdown-name-collision.json +++ b/mobile/rpc-foundation/goldens/session-create-markdown-name-collision.json @@ -3,7 +3,7 @@ "family": "session.content-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-create-markdown-note.json b/mobile/rpc-foundation/goldens/session-create-markdown-note.json index f9ee9130843..897c5db1e00 100644 --- a/mobile/rpc-foundation/goldens/session-create-markdown-note.json +++ b/mobile/rpc-foundation/goldens/session-create-markdown-note.json @@ -3,7 +3,7 @@ "family": "session.content-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-create-terminal-ignores-a-second-create-in-flight.json b/mobile/rpc-foundation/goldens/session-create-terminal-ignores-a-second-create-in-flight.json index 584f77e46bf..19b65dfcb13 100644 --- a/mobile/rpc-foundation/goldens/session-create-terminal-ignores-a-second-create-in-flight.json +++ b/mobile/rpc-foundation/goldens/session-create-terminal-ignores-a-second-create-in-flight.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/session-create-terminal-launches-an-agent-quick-command.json b/mobile/rpc-foundation/goldens/session-create-terminal-launches-an-agent-quick-command.json index c92a2099aa0..859c60a4576 100644 --- a/mobile/rpc-foundation/goldens/session-create-terminal-launches-an-agent-quick-command.json +++ b/mobile/rpc-foundation/goldens/session-create-terminal-launches-an-agent-quick-command.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/session-create-terminal-refused.json b/mobile/rpc-foundation/goldens/session-create-terminal-refused.json index cb87f9b0626..c8c87e93e2e 100644 --- a/mobile/rpc-foundation/goldens/session-create-terminal-refused.json +++ b/mobile/rpc-foundation/goldens/session-create-terminal-refused.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/session-create-terminal-replaces-active.json b/mobile/rpc-foundation/goldens/session-create-terminal-replaces-active.json index 85cd835be53..abc4f0ab3c0 100644 --- a/mobile/rpc-foundation/goldens/session-create-terminal-replaces-active.json +++ b/mobile/rpc-foundation/goldens/session-create-terminal-replaces-active.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/session-create-terminal-runs-a-quick-command.json b/mobile/rpc-foundation/goldens/session-create-terminal-runs-a-quick-command.json index 1abfaa447bb..43bda3f3618 100644 --- a/mobile/rpc-foundation/goldens/session-create-terminal-runs-a-quick-command.json +++ b/mobile/rpc-foundation/goldens/session-create-terminal-runs-a-quick-command.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/session-create-terminal-with-prompt.json b/mobile/rpc-foundation/goldens/session-create-terminal-with-prompt.json index 56522d73be0..78febc06d30 100644 --- a/mobile/rpc-foundation/goldens/session-create-terminal-with-prompt.json +++ b/mobile/rpc-foundation/goldens/session-create-terminal-with-prompt.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/session-create-terminal-without-active-tab.json b/mobile/rpc-foundation/goldens/session-create-terminal-without-active-tab.json index cab89fc5bc0..8b94f47d641 100644 --- a/mobile/rpc-foundation/goldens/session-create-terminal-without-active-tab.json +++ b/mobile/rpc-foundation/goldens/session-create-terminal-without-active-tab.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/session-create-terminal-without-handle.json b/mobile/rpc-foundation/goldens/session-create-terminal-without-handle.json index 25a3fb7a07a..b02219910d5 100644 --- a/mobile/rpc-foundation/goldens/session-create-terminal-without-handle.json +++ b/mobile/rpc-foundation/goldens/session-create-terminal-without-handle.json @@ -3,7 +3,7 @@ "family": "session.create-terminal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fa7d9fd6428e89282f08e04fefba4289000eb3aed1462489a2f11efed374382c", diff --git a/mobile/rpc-foundation/goldens/session-diff-notes-load-refused.json b/mobile/rpc-foundation/goldens/session-diff-notes-load-refused.json index b6c37d3dfe6..fdb74097444 100644 --- a/mobile/rpc-foundation/goldens/session-diff-notes-load-refused.json +++ b/mobile/rpc-foundation/goldens/session-diff-notes-load-refused.json @@ -3,7 +3,7 @@ "family": "session.diff-notes", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/session-diff-notes-loaded.json b/mobile/rpc-foundation/goldens/session-diff-notes-loaded.json index e97f6f3a88e..7887cdac1a8 100644 --- a/mobile/rpc-foundation/goldens/session-diff-notes-loaded.json +++ b/mobile/rpc-foundation/goldens/session-diff-notes-loaded.json @@ -3,7 +3,7 @@ "family": "session.diff-notes", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/session-file-tab-read.json b/mobile/rpc-foundation/goldens/session-file-tab-read.json index 86d3316b451..379452e0d6d 100644 --- a/mobile/rpc-foundation/goldens/session-file-tab-read.json +++ b/mobile/rpc-foundation/goldens/session-file-tab-read.json @@ -3,7 +3,7 @@ "family": "session.tab-documents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-markdown-disk-read.json b/mobile/rpc-foundation/goldens/session-markdown-disk-read.json new file mode 100644 index 00000000000..de82fdf8fa2 --- /dev/null +++ b/mobile/rpc-foundation/goldens/session-markdown-disk-read.json @@ -0,0 +1,140 @@ +{ + "operation": "session.tab-documents", + "family": "session.markdown-disk-fallback", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", + "scenarioSha256": "29565833b19968faa701f656d166050419f149d6bb76d1f63f4dd3af76b8e896", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "44abbe2af7aa": { + "name": "markdown.readTab#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"markdown.readTab\",\"params\":{\"worktree\":\"id:workspace-1\",\"tabId\":\"tab-md\"}}" + }, + "c6cea5310098": { + "file": {}, + "markdown": { + "tab-md": { + "baseVersion": "", + "content": "# disk", + "editable": false, + "isDirty": false, + "localContent": "# disk", + "readOnlyReason": "Editing needs Orca desktop running.", + "stale": false, + "status": "ready" + } + } + }, + "d4be01606497": { + "name": "files.read#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + }, + "d59404d25d6c": { + "name": "files.read#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "byteLength": 6, + "content": "# disk", + "truncated": false + } + } + } + }, + "e258dafc91f1": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "renderer_unavailable", + "message": "Renderer unavailable" + }, + "id": "frame-1", + "ok": false + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "session-markdown-disk-read", + "checkpoints": [ + { + "id": "fell-back", + "observation": { + "sender": ["e258dafc91f1", "d59404d25d6c"], + "payloads": ["44abbe2af7aa", "d4be01606497"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "c6cea5310098", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/session-markdown-disk-served.json b/mobile/rpc-foundation/goldens/session-markdown-disk-served.json new file mode 100644 index 00000000000..b52f4e9a2c3 --- /dev/null +++ b/mobile/rpc-foundation/goldens/session-markdown-disk-served.json @@ -0,0 +1,102 @@ +{ + "operation": "session.tab-documents", + "family": "session.markdown-disk-fallback", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", + "scenarioSha256": "8375df17afabf282a1d37ffaa3a334b1fa9105ba04e6618f16082f66bfe3f76d", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0d6b0adc036f": { + "name": "markdown.readTab#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "markdown.readTab" + }, + { + "name": "params", + "value": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "content": "# served", + "editable": true, + "isDirty": false, + "version": "v1" + } + } + } + }, + "3e3efaab3a20": { + "file": {}, + "markdown": { + "tab-md": { + "baseVersion": "v1", + "content": "# served", + "editable": true, + "isDirty": false, + "localContent": "# served", + "readOnlyReason": { + "$rpc": "undefined" + }, + "stale": false, + "status": "ready" + } + } + }, + "44abbe2af7aa": { + "name": "markdown.readTab#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"markdown.readTab\",\"params\":{\"worktree\":\"id:workspace-1\",\"tabId\":\"tab-md\"}}" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "session-markdown-disk-served", + "checkpoints": [ + { + "id": "served", + "observation": { + "sender": ["0d6b0adc036f"], + "payloads": ["44abbe2af7aa"], + "settlements": { + "markdown": "eb79a9b3682a" + }, + "state": "3e3efaab3a20", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/session-markdown-save-conflict.json b/mobile/rpc-foundation/goldens/session-markdown-save-conflict.json index 8f4990d42eb..520513b7533 100644 --- a/mobile/rpc-foundation/goldens/session-markdown-save-conflict.json +++ b/mobile/rpc-foundation/goldens/session-markdown-save-conflict.json @@ -3,7 +3,7 @@ "family": "session.markdown-save", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/session-markdown-saved.json b/mobile/rpc-foundation/goldens/session-markdown-saved.json index b1435b3b624..c8a05d25441 100644 --- a/mobile/rpc-foundation/goldens/session-markdown-saved.json +++ b/mobile/rpc-foundation/goldens/session-markdown-saved.json @@ -3,7 +3,7 @@ "family": "session.markdown-save", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/session-markdown-tab-disk-fallback.json b/mobile/rpc-foundation/goldens/session-markdown-tab-disk-fallback.json index 506f1011d85..91707c3827d 100644 --- a/mobile/rpc-foundation/goldens/session-markdown-tab-disk-fallback.json +++ b/mobile/rpc-foundation/goldens/session-markdown-tab-disk-fallback.json @@ -3,7 +3,7 @@ "family": "session.tab-documents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-markdown-tab-read.json b/mobile/rpc-foundation/goldens/session-markdown-tab-read.json index 7dcb8ebfcc9..ab9580fb2be 100644 --- a/mobile/rpc-foundation/goldens/session-markdown-tab-read.json +++ b/mobile/rpc-foundation/goldens/session-markdown-tab-read.json @@ -3,7 +3,7 @@ "family": "session.tab-documents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-markdown-tab-refused.json b/mobile/rpc-foundation/goldens/session-markdown-tab-refused.json index 22cfe8a4cef..0881ac812f3 100644 --- a/mobile/rpc-foundation/goldens/session-markdown-tab-refused.json +++ b/mobile/rpc-foundation/goldens/session-markdown-tab-refused.json @@ -3,7 +3,7 @@ "family": "session.tab-documents", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-startup-both-activation-sites.json b/mobile/rpc-foundation/goldens/session-startup-both-activation-sites.json index 391d9e45091..56ac593798e 100644 --- a/mobile/rpc-foundation/goldens/session-startup-both-activation-sites.json +++ b/mobile/rpc-foundation/goldens/session-startup-both-activation-sites.json @@ -3,7 +3,7 @@ "family": "session.startup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "6b08c394e37fd572cf63a4c11934247d379008f11117aa5af33b2926fbd32d1e", diff --git a/mobile/rpc-foundation/goldens/session-startup-floating-route-skips-activation.json b/mobile/rpc-foundation/goldens/session-startup-floating-route-skips-activation.json index f32c7d9868a..87c42acaedc 100644 --- a/mobile/rpc-foundation/goldens/session-startup-floating-route-skips-activation.json +++ b/mobile/rpc-foundation/goldens/session-startup-floating-route-skips-activation.json @@ -3,7 +3,7 @@ "family": "session.startup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "6b08c394e37fd572cf63a4c11934247d379008f11117aa5af33b2926fbd32d1e", diff --git a/mobile/rpc-foundation/goldens/session-startup-keeps-terminals-visible-on-reconnect.json b/mobile/rpc-foundation/goldens/session-startup-keeps-terminals-visible-on-reconnect.json index 2066dffd10e..4647e085c44 100644 --- a/mobile/rpc-foundation/goldens/session-startup-keeps-terminals-visible-on-reconnect.json +++ b/mobile/rpc-foundation/goldens/session-startup-keeps-terminals-visible-on-reconnect.json @@ -3,7 +3,7 @@ "family": "session.startup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "6b08c394e37fd572cf63a4c11934247d379008f11117aa5af33b2926fbd32d1e", diff --git a/mobile/rpc-foundation/goldens/session-startup-refused-tab-load-still-loads-terminals.json b/mobile/rpc-foundation/goldens/session-startup-refused-tab-load-still-loads-terminals.json index 855a752a8eb..7cf9af5e559 100644 --- a/mobile/rpc-foundation/goldens/session-startup-refused-tab-load-still-loads-terminals.json +++ b/mobile/rpc-foundation/goldens/session-startup-refused-tab-load-still-loads-terminals.json @@ -3,7 +3,7 @@ "family": "session.startup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "6b08c394e37fd572cf63a4c11934247d379008f11117aa5af33b2926fbd32d1e", diff --git a/mobile/rpc-foundation/goldens/session-tab-activation-focus-and-activate.json b/mobile/rpc-foundation/goldens/session-tab-activation-focus-and-activate.json index 851ab342a43..e6d67fb3c7c 100644 --- a/mobile/rpc-foundation/goldens/session-tab-activation-focus-and-activate.json +++ b/mobile/rpc-foundation/goldens/session-tab-activation-focus-and-activate.json @@ -3,7 +3,7 @@ "family": "session.tab-activation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/session-tab-activation-refused.json b/mobile/rpc-foundation/goldens/session-tab-activation-refused.json index 65428d7bcac..83cdc6521ca 100644 --- a/mobile/rpc-foundation/goldens/session-tab-activation-refused.json +++ b/mobile/rpc-foundation/goldens/session-tab-activation-refused.json @@ -3,7 +3,7 @@ "family": "session.tab-activation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/session-tab-activation-transport-error.json b/mobile/rpc-foundation/goldens/session-tab-activation-transport-error.json index f38d615ca93..155d32f08e9 100644 --- a/mobile/rpc-foundation/goldens/session-tab-activation-transport-error.json +++ b/mobile/rpc-foundation/goldens/session-tab-activation-transport-error.json @@ -3,7 +3,7 @@ "family": "session.tab-activation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json b/mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json index 889782a0ed6..6b017291d2a 100644 --- a/mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json +++ b/mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json @@ -3,7 +3,7 @@ "family": "session.tab-close", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tab-close-session-tab.json b/mobile/rpc-foundation/goldens/session-tab-close-session-tab.json index 0d7736cb405..6b60979b207 100644 --- a/mobile/rpc-foundation/goldens/session-tab-close-session-tab.json +++ b/mobile/rpc-foundation/goldens/session-tab-close-session-tab.json @@ -3,7 +3,7 @@ "family": "session.tab-close", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tab-close-terminal.json b/mobile/rpc-foundation/goldens/session-tab-close-terminal.json index a96e597d224..27cb3712c02 100644 --- a/mobile/rpc-foundation/goldens/session-tab-close-terminal.json +++ b/mobile/rpc-foundation/goldens/session-tab-close-terminal.json @@ -3,7 +3,7 @@ "family": "session.tab-close", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tab-closed.json b/mobile/rpc-foundation/goldens/session-tab-closed.json index 38d9c8713da..b8c85ed3945 100644 --- a/mobile/rpc-foundation/goldens/session-tab-closed.json +++ b/mobile/rpc-foundation/goldens/session-tab-closed.json @@ -3,7 +3,7 @@ "family": "session.tab-close-session", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tab-rename.json b/mobile/rpc-foundation/goldens/session-tab-rename.json index 091f028d644..28c6d985885 100644 --- a/mobile/rpc-foundation/goldens/session-tab-rename.json +++ b/mobile/rpc-foundation/goldens/session-tab-rename.json @@ -3,7 +3,7 @@ "family": "session.tab-close", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tab-renamed.json b/mobile/rpc-foundation/goldens/session-tab-renamed.json index 395cf9e72ed..aef86e0995d 100644 --- a/mobile/rpc-foundation/goldens/session-tab-renamed.json +++ b/mobile/rpc-foundation/goldens/session-tab-renamed.json @@ -3,7 +3,7 @@ "family": "session.tab-rename", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tabs-health-errored.json b/mobile/rpc-foundation/goldens/session-tabs-health-errored.json index 977089af9f4..cfa8953d600 100644 --- a/mobile/rpc-foundation/goldens/session-tabs-health-errored.json +++ b/mobile/rpc-foundation/goldens/session-tabs-health-errored.json @@ -3,7 +3,7 @@ "family": "session.tabs-stream-health", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/session-tabs-health-reconciled.json b/mobile/rpc-foundation/goldens/session-tabs-health-reconciled.json index f866c52de56..ec52eac1992 100644 --- a/mobile/rpc-foundation/goldens/session-tabs-health-reconciled.json +++ b/mobile/rpc-foundation/goldens/session-tabs-health-reconciled.json @@ -3,7 +3,7 @@ "family": "session.tabs-stream-health", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/session-tabs-health-refused.json b/mobile/rpc-foundation/goldens/session-tabs-health-refused.json index 55f081b55e1..330c8becc32 100644 --- a/mobile/rpc-foundation/goldens/session-tabs-health-refused.json +++ b/mobile/rpc-foundation/goldens/session-tabs-health-refused.json @@ -3,7 +3,7 @@ "family": "session.tabs-stream-health", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/session-tabs-health-stale-application-revision.json b/mobile/rpc-foundation/goldens/session-tabs-health-stale-application-revision.json index fdd90ebe14a..a544d94becf 100644 --- a/mobile/rpc-foundation/goldens/session-tabs-health-stale-application-revision.json +++ b/mobile/rpc-foundation/goldens/session-tabs-health-stale-application-revision.json @@ -3,7 +3,7 @@ "family": "session.tabs-stream-health", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4a1e81ab3229c8fd10b3ad435568efec11a944e0f02a183f94e4b1f44a7e5de0", diff --git a/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-take-floor.json b/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-take-floor.json index 8656a67a836..65429ed3703 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-take-floor.json +++ b/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-take-floor.json @@ -3,7 +3,7 @@ "family": "session.terminal-display-mode", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9e90ad39a8d4257adf30166757a6364c4710dc3ae9f06365f80a3dd8f1c94d89", diff --git a/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-device-token.json b/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-device-token.json index 1d0dc12e51d..e0390068e7d 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-device-token.json +++ b/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-device-token.json @@ -3,7 +3,7 @@ "family": "session.terminal-display-mode", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9e90ad39a8d4257adf30166757a6364c4710dc3ae9f06365f80a3dd8f1c94d89", diff --git a/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-viewport.json b/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-viewport.json index 46ef4b161f4..929156e84e7 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-viewport.json +++ b/mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-viewport.json @@ -3,7 +3,7 @@ "family": "session.terminal-display-mode", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9e90ad39a8d4257adf30166757a6364c4710dc3ae9f06365f80a3dd8f1c94d89", diff --git a/mobile/rpc-foundation/goldens/session-terminal-display-mode-drops-second-toggle.json b/mobile/rpc-foundation/goldens/session-terminal-display-mode-drops-second-toggle.json index e02e028f749..6697f43c705 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-display-mode-drops-second-toggle.json +++ b/mobile/rpc-foundation/goldens/session-terminal-display-mode-drops-second-toggle.json @@ -3,7 +3,7 @@ "family": "session.terminal-display-mode", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9e90ad39a8d4257adf30166757a6364c4710dc3ae9f06365f80a3dd8f1c94d89", diff --git a/mobile/rpc-foundation/goldens/session-terminal-display-mode-to-desktop.json b/mobile/rpc-foundation/goldens/session-terminal-display-mode-to-desktop.json index ee773e53988..2449bb28d63 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-display-mode-to-desktop.json +++ b/mobile/rpc-foundation/goldens/session-terminal-display-mode-to-desktop.json @@ -3,7 +3,7 @@ "family": "session.terminal-display-mode", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9e90ad39a8d4257adf30166757a6364c4710dc3ae9f06365f80a3dd8f1c94d89", diff --git a/mobile/rpc-foundation/goldens/session-terminal-list-dedupes-handles.json b/mobile/rpc-foundation/goldens/session-terminal-list-dedupes-handles.json index 29798870843..2bad4800947 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-list-dedupes-handles.json +++ b/mobile/rpc-foundation/goldens/session-terminal-list-dedupes-handles.json @@ -3,7 +3,7 @@ "family": "session.terminal-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-terminal-list-empty-guarded.json b/mobile/rpc-foundation/goldens/session-terminal-list-empty-guarded.json index eb960094204..e2b209182a6 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-list-empty-guarded.json +++ b/mobile/rpc-foundation/goldens/session-terminal-list-empty-guarded.json @@ -3,7 +3,7 @@ "family": "session.terminal-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-terminal-list-merged.json b/mobile/rpc-foundation/goldens/session-terminal-list-merged.json index 2757872ee30..a562b814680 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-list-merged.json +++ b/mobile/rpc-foundation/goldens/session-terminal-list-merged.json @@ -3,7 +3,7 @@ "family": "session.terminal-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-terminal-list-refused.json b/mobile/rpc-foundation/goldens/session-terminal-list-refused.json index ee75e846dc7..3719f1e6e0f 100644 --- a/mobile/rpc-foundation/goldens/session-terminal-list-refused.json +++ b/mobile/rpc-foundation/goldens/session-terminal-list-refused.json @@ -3,7 +3,7 @@ "family": "session.terminal-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json b/mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json index 4a81d9d6bc0..67600385781 100644 --- a/mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json b/mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json index 20cd472fcd7..a692328dc6b 100644 --- a/mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json +++ b/mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json @@ -3,7 +3,7 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json b/mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json index 3379ad2ddbf..28d5aa7b492 100644 --- a/mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json +++ b/mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json @@ -3,7 +3,7 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json b/mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json index 1d6e3705b92..5acc7dc254f 100644 --- a/mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json @@ -3,7 +3,7 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-home-coalesced.json b/mobile/rpc-foundation/goldens/settings-home-coalesced.json index cff3a0f248f..607ceb8f96e 100644 --- a/mobile/rpc-foundation/goldens/settings-home-coalesced.json +++ b/mobile/rpc-foundation/goldens/settings-home-coalesced.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json b/mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json index 253e3a90cb4..965b2a51383 100644 --- a/mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json index 12398d49e62..5e678441b0a 100644 --- a/mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-home-providers-refused.json b/mobile/rpc-foundation/goldens/settings-home-providers-refused.json index 00ced816288..fa6d25c24f9 100644 --- a/mobile/rpc-foundation/goldens/settings-home-providers-refused.json +++ b/mobile/rpc-foundation/goldens/settings-home-providers-refused.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json b/mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json index ecfdfe5479d..318203dc3b8 100644 --- a/mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json @@ -3,7 +3,7 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-new-tab-refused.json b/mobile/rpc-foundation/goldens/settings-new-tab-refused.json index 6cdcd10c8a4..899f5a41bb2 100644 --- a/mobile/rpc-foundation/goldens/settings-new-tab-refused.json +++ b/mobile/rpc-foundation/goldens/settings-new-tab-refused.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/settings-new-tab-ssh.json b/mobile/rpc-foundation/goldens/settings-new-tab-ssh.json index 735352598d4..98f1ad0abb2 100644 --- a/mobile/rpc-foundation/goldens/settings-new-tab-ssh.json +++ b/mobile/rpc-foundation/goldens/settings-new-tab-ssh.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json b/mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json index 81ebb8d2245..760fb48bd37 100644 --- a/mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json @@ -3,7 +3,7 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", diff --git a/mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json b/mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json index 26c5fa79814..a81fa8fc825 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json +++ b/mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json index 396c5db71b8..84a4f4d8498 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json new file mode 100644 index 00000000000..6b078c2a563 --- /dev/null +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json @@ -0,0 +1,469 @@ +{ + "operation": "settings.repo-metadata", + "family": "settings.repo-metadata", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", + "scenarioSha256": "e67b645d8c4fa42e93d4cf36f45311922f7b9d9d8cc597a12f55c664d840901b", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "12ba3f41d4db": { + "name": "hostPlatform", + "ordinal": 14, + "value": "darwin" + }, + "19b2979d7fc2": { + "name": "repo.list#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}" + }, + "19bf6d2469a6": { + "name": "repoColorsByName", + "ordinal": 3, + "value": [ + ["Lucide", "#123456"], + ["Emoji", "#f97316"], + ["Avatar", "#f97316"] + ] + }, + "1a160fc2d137": { + "name": "host.platform#1", + "ordinal": 9, + "args": [ + { + "name": "method", + "value": "host.platform" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "36a96ddbd396": { + "name": "host.platform#1", + "ordinal": 12, + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}" + }, + "56ab81cb67dc": { + "name": "hostLabelById", + "ordinal": 13, + "value": [["ssh:ssh-1", "SSH"]] + }, + "6338d57f282f": { + "repoColorsByName": [ + ["Lucide", "#123456"], + ["Emoji", "#f97316"], + ["Avatar", "#f97316"] + ], + "repoHostIdByRepoId": [ + ["repo-1", "local"], + ["repo-2", "ssh:ssh-1"], + ["repo-3", "ssh:ssh-1"] + ], + "repoIconsByName": [ + [ + "Lucide", + { + "name": "Rocket", + "type": "lucide" + } + ], + [ + "Emoji", + { + "emoji": "🐳", + "type": "emoji" + } + ], + [ + "Avatar", + { + "label": "acme/orca", + "source": "github", + "src": "https://github.com/acme.png?size=64", + "type": "image" + } + ] + ], + "repoIdsByName": [ + ["Lucide", "repo-1"], + ["Emoji", "repo-2"], + ["Avatar", "repo-3"] + ] + }, + "64f5522cc303": { + "name": "settings.get#1", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [], + "hostSettingOverrides": {} + } + } + } + } + }, + "668a7e313975": { + "name": "ssh.listTargetSummaries#1", + "ordinal": 10, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}" + }, + "6bc5e5b7a259": { + "name": "repoHostIdByRepoId", + "ordinal": 6, + "value": [ + ["repo-1", "local"], + ["repo-2", "ssh:ssh-1"], + ["repo-3", "ssh:ssh-1"] + ] + }, + "77cbfd1f087c": { + "name": "repoIdsByName", + "ordinal": 5, + "value": [ + ["Lucide", "repo-1"], + ["Emoji", "repo-2"], + ["Avatar", "repo-3"] + ] + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "96e0c54d86b0": { + "name": "settings.get#1", + "ordinal": 8, + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "a9add325bb0a": { + "name": "repo.list#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "repo.list" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "repos": [ + { + "badgeColor": "#123456", + "connectionId": { + "$rpc": "null" + }, + "displayName": "Lucide", + "id": "repo-1", + "repoIcon": { + "name": "Rocket", + "type": "lucide" + } + }, + { + "connectionId": "ssh-1", + "displayName": "Emoji", + "id": "repo-2", + "repoIcon": { + "emoji": "🐳", + "type": "emoji" + } + }, + { + "displayName": "Avatar", + "executionHostId": "ssh:ssh-1", + "id": "repo-3", + "repoIcon": { + "label": "acme/orca", + "source": "github", + "src": "https://github.com/acme.png?size=64", + "type": "image" + } + } + ] + } + } + } + }, + "ae27a5b525fa": { + "name": "repoIconsByName", + "ordinal": 4, + "value": [ + [ + "Lucide", + { + "name": "Rocket", + "type": "lucide" + } + ], + [ + "Emoji", + { + "emoji": "🐳", + "type": "emoji" + } + ], + [ + "Avatar", + { + "label": "acme/orca", + "source": "github", + "src": "https://github.com/acme.png?size=64", + "type": "image" + } + ] + ] + }, + "b428bce85ee7": { + "name": "settings.get#1", + "ordinal": 11, + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" + }, + "c752e67787f9": { + "name": "ssh.listTargetSummaries#1", + "ordinal": 7, + "args": [ + { + "name": "method", + "value": "ssh.listTargetSummaries" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "targets": [ + { + "id": "ssh-1", + "label": "SSH" + } + ] + } + } + } + }, + "e62b46a04ba1": { + "hostLabelById": [["ssh:ssh-1", "SSH"]], + "hostPlatform": "darwin", + "repoColorsByName": [ + ["Lucide", "#123456"], + ["Emoji", "#f97316"], + ["Avatar", "#f97316"] + ], + "repoHostIdByRepoId": [ + ["repo-1", "local"], + ["repo-2", "ssh:ssh-1"], + ["repo-3", "ssh:ssh-1"] + ], + "repoIconsByName": [ + [ + "Lucide", + { + "name": "Rocket", + "type": "lucide" + } + ], + [ + "Emoji", + { + "emoji": "🐳", + "type": "emoji" + } + ], + [ + "Avatar", + { + "label": "acme/orca", + "source": "github", + "src": "https://github.com/acme.png?size=64", + "type": "image" + } + ] + ], + "repoIdsByName": [ + ["Lucide", "repo-1"], + ["Emoji", "repo-2"], + ["Avatar", "repo-3"] + ] + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ee8e16df5187": { + "name": "host.platform#1", + "ordinal": 9, + "args": [ + { + "name": "method", + "value": "host.platform" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "platform": "darwin" + } + } + } + } + }, + "recording": { + "scenario": "settings-repo-metadata-icons", + "checkpoints": [ + { + "id": "settings-pending", + "observation": { + "sender": ["a9add325bb0a", "c752e67787f9", "96e0c54d86b0", "1a160fc2d137"], + "payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"], + "settlements": { + "mount": "eb79a9b3682a", + "load": "9270aeb7d9c6" + }, + "state": "6338d57f282f", + "effects": ["19bf6d2469a6", "ae27a5b525fa", "77cbfd1f087c", "6bc5e5b7a259"] + } + }, + { + "id": "settled", + "observation": { + "sender": ["a9add325bb0a", "c752e67787f9", "64f5522cc303", "ee8e16df5187"], + "payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"], + "settlements": { + "mount": "eb79a9b3682a", + "load": "eb79a9b3682a" + }, + "state": "e62b46a04ba1", + "effects": [ + "19bf6d2469a6", + "ae27a5b525fa", + "77cbfd1f087c", + "6bc5e5b7a259", + "56ab81cb67dc", + "12ba3f41d4db" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json index 63609ba7f5d..c1e24d6d268 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json index 365e9fe7457..a47257b0c59 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json index 6385c79af0f..f3a7f6c397c 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json index 1103c1a2888..a385b5cb36d 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json b/mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json index db67b17d9f8..7f171eb0637 100644 --- a/mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json index 38255a65b74..b610203208b 100644 --- a/mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json b/mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json index 778d0b59b80..058901c2d7c 100644 --- a/mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json +++ b/mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json b/mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json index f1459fa4f81..0b42ea1231e 100644 --- a/mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json @@ -3,7 +3,7 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json b/mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json index 1b8de276f0d..90949ed775a 100644 --- a/mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json index d4d57ada256..c18e3069c45 100644 --- a/mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-task-hydration-refused.json b/mobile/rpc-foundation/goldens/settings-task-hydration-refused.json index 045bf076e6b..5c4f2b0a2f2 100644 --- a/mobile/rpc-foundation/goldens/settings-task-hydration-refused.json +++ b/mobile/rpc-foundation/goldens/settings-task-hydration-refused.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json b/mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json index cfef7446c4e..53c8b4b28a3 100644 --- a/mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json @@ -3,7 +3,7 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json b/mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json index e02c9a604fd..21abaa2256f 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json @@ -3,7 +3,7 @@ "family": "settings.task-workspace-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json b/mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json index 3c54db00768..7cddc3196a6 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json @@ -3,7 +3,7 @@ "family": "settings.task-workspace-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json b/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json index 7b39d1a33cf..04481fdd1e0 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.task-workspace", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json b/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json index 3685477977b..7a9e5b06aed 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json @@ -3,7 +3,7 @@ "family": "settings.task-workspace", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json b/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json index 3dabde8d5fa..25f63e2876d 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json @@ -3,7 +3,7 @@ "family": "settings.task-workspace", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/settings-task-write.json b/mobile/rpc-foundation/goldens/settings-task-write.json index d6ea3af2dd1..a4e31adf30e 100644 --- a/mobile/rpc-foundation/goldens/settings-task-write.json +++ b/mobile/rpc-foundation/goldens/settings-task-write.json @@ -3,7 +3,7 @@ "family": "settings-best-effort", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json b/mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json index facfc0760e8..20d79753238 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json index 34603137d44..a2b4543745f 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-context-refused.json b/mobile/rpc-foundation/goldens/settings-workspace-context-refused.json index 1251fcfc718..8c40c37ea97 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-context-refused.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-context-refused.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json b/mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json index a57ff030e7b..2808fb6c0f3 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json @@ -3,7 +3,7 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json index 21e6d38fe69..72f0520ce70 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json @@ -3,7 +3,7 @@ "family": "settings.workspace-submit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json index ed804b3dcc1..82684ec2ec7 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json @@ -3,7 +3,7 @@ "family": "settings.workspace-submit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json index c50a2e730a2..33e1ca27c8a 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json @@ -3,7 +3,7 @@ "family": "settings.workspace-submit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json b/mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json index ab73718c9cf..f85a254717c 100644 --- a/mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json +++ b/mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json @@ -3,7 +3,7 @@ "family": "speech.dictation-chunk", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json b/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json index a11fdc81625..59a78b492fe 100644 --- a/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json +++ b/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json @@ -3,7 +3,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json b/mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json index cff9f935713..953ac766d09 100644 --- a/mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json +++ b/mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json @@ -3,7 +3,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json b/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json index 57f131852c6..9f329ccd7cb 100644 --- a/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json +++ b/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json @@ -3,7 +3,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json b/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json index 2e488051279..f8492c20525 100644 --- a/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json +++ b/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json @@ -3,7 +3,7 @@ "family": "speech.dictation-session", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json b/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json index cc749827b4c..dc5a9db18db 100644 --- a/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json +++ b/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json @@ -3,7 +3,7 @@ "family": "speech.dictation-session", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-setup-sheet-denied-to-mobile.json b/mobile/rpc-foundation/goldens/speech-setup-sheet-denied-to-mobile.json index d80e188cf1a..39188439a58 100644 --- a/mobile/rpc-foundation/goldens/speech-setup-sheet-denied-to-mobile.json +++ b/mobile/rpc-foundation/goldens/speech-setup-sheet-denied-to-mobile.json @@ -3,7 +3,7 @@ "family": "speech.setup-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-setup-sheet-fulfilled.json b/mobile/rpc-foundation/goldens/speech-setup-sheet-fulfilled.json index a2ad5d1d754..eda46d8d76d 100644 --- a/mobile/rpc-foundation/goldens/speech-setup-sheet-fulfilled.json +++ b/mobile/rpc-foundation/goldens/speech-setup-sheet-fulfilled.json @@ -3,7 +3,7 @@ "family": "speech.setup-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-setup-sheet-legacy-desktop.json b/mobile/rpc-foundation/goldens/speech-setup-sheet-legacy-desktop.json index da4e4c6a1db..8c843be7ef4 100644 --- a/mobile/rpc-foundation/goldens/speech-setup-sheet-legacy-desktop.json +++ b/mobile/rpc-foundation/goldens/speech-setup-sheet-legacy-desktop.json @@ -3,7 +3,7 @@ "family": "speech.setup-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", diff --git a/mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json b/mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json new file mode 100644 index 00000000000..89ad5b48ce2 --- /dev/null +++ b/mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json @@ -0,0 +1,164 @@ +{ + "operation": "speech.setup-sheet", + "family": "speech.setup-sheet", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", + "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", + "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", + "adapterSha256": "3ddb44511547ae2fc97340e5a49393233f6716f9704b7b994f43bc030788b25b", + "scenarioSha256": "88e563aa087955b9ef6447b7e73711f90d5befb7c082270da58695c3a7169e67", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "430a60866d83": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dictationMode": "hold", + "enabled": true, + "models": [ + { + "id": "whisper-small", + "label": "Small", + "progress": 0.42, + "provider": "local", + "recommended": true, + "sizeBytes": 466862080, + "status": "downloading" + }, + { + "id": "gpt-4o-transcribe", + "label": "OpenAI", + "progress": { + "$rpc": "null" + }, + "provider": "openai", + "recommended": false, + "sizeBytes": { + "$rpc": "null" + }, + "status": "ready" + } + ], + "selectedModelId": "gpt-4o-transcribe" + } + }, + "4f793e38be31": { + "name": "speech.models.list#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.models.list\",\"params\":null}" + }, + "d91e455b7326": { + "list": { + "dictationMode": "hold", + "enabled": true, + "models": [ + { + "id": "whisper-small", + "label": "Small", + "progress": 0.42, + "provider": "local", + "recommended": true, + "sizeBytes": 466862080, + "status": "downloading" + }, + { + "id": "gpt-4o-transcribe", + "label": "OpenAI", + "progress": { + "$rpc": "null" + }, + "provider": "openai", + "recommended": false, + "sizeBytes": { + "$rpc": "null" + }, + "status": "ready" + } + ], + "selectedModelId": "gpt-4o-transcribe" + } + }, + "f4bd63aa8e8f": { + "name": "speech.models.list#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.models.list" + }, + { + "name": "params", + "value": { + "$rpc": "null" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "dictationMode": "hold", + "enabled": true, + "models": [ + { + "id": "whisper-small", + "label": "Small", + "progress": 0.42, + "provider": "local", + "recommended": true, + "sizeBytes": 466862080, + "status": "downloading" + }, + { + "id": "gpt-4o-transcribe", + "label": "OpenAI", + "progress": { + "$rpc": "null" + }, + "provider": "openai", + "recommended": false, + "sizeBytes": { + "$rpc": "null" + }, + "status": "ready" + } + ], + "selectedModelId": "gpt-4o-transcribe" + } + } + } + } + }, + "recording": { + "scenario": "speech-setup-sheet-model-vocabulary", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["f4bd63aa8e8f"], + "payloads": ["4f793e38be31"], + "settlements": { + "list": "430a60866d83" + }, + "state": "d91e455b7326", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/structured-agent-session-created.json b/mobile/rpc-foundation/goldens/structured-agent-session-created.json index 479f7b46b55..ae657fd7af0 100644 --- a/mobile/rpc-foundation/goldens/structured-agent-session-created.json +++ b/mobile/rpc-foundation/goldens/structured-agent-session-created.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/structured-launch-created.json b/mobile/rpc-foundation/goldens/structured-launch-created.json index 1b1f6a1d9bf..b5ff53f74bc 100644 --- a/mobile/rpc-foundation/goldens/structured-launch-created.json +++ b/mobile/rpc-foundation/goldens/structured-launch-created.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/structured-launch-definitive-refusal.json b/mobile/rpc-foundation/goldens/structured-launch-definitive-refusal.json index 098b18e6278..d51c62db122 100644 --- a/mobile/rpc-foundation/goldens/structured-launch-definitive-refusal.json +++ b/mobile/rpc-foundation/goldens/structured-launch-definitive-refusal.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/structured-launch-replays-dropped-create.json b/mobile/rpc-foundation/goldens/structured-launch-replays-dropped-create.json index fbda5747fda..9f9a9c51d46 100644 --- a/mobile/rpc-foundation/goldens/structured-launch-replays-dropped-create.json +++ b/mobile/rpc-foundation/goldens/structured-launch-replays-dropped-create.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/structured-launch-support-refused.json b/mobile/rpc-foundation/goldens/structured-launch-support-refused.json index 6838f1d33e4..a277bf59c43 100644 --- a/mobile/rpc-foundation/goldens/structured-launch-support-refused.json +++ b/mobile/rpc-foundation/goldens/structured-launch-support-refused.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/structured-launch-unsupported.json b/mobile/rpc-foundation/goldens/structured-launch-unsupported.json index a30c2aaf617..851a6426370 100644 --- a/mobile/rpc-foundation/goldens/structured-launch-unsupported.json +++ b/mobile/rpc-foundation/goldens/structured-launch-unsupported.json @@ -3,7 +3,7 @@ "family": "agentSession.structured-launch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/tasks-route-repo-list.json b/mobile/rpc-foundation/goldens/tasks-route-repo-list.json index ed4581fada9..61e02631a16 100644 --- a/mobile/rpc-foundation/goldens/tasks-route-repo-list.json +++ b/mobile/rpc-foundation/goldens/tasks-route-repo-list.json @@ -3,7 +3,7 @@ "family": "tasks.route-repo-list", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "feb6cee1ab7ecff1ba98bfba22d4924c748d3bb6b749db460cb617ee50b92f2c", diff --git a/mobile/rpc-foundation/goldens/terminal-gesture-flush-and-clear.json b/mobile/rpc-foundation/goldens/terminal-gesture-flush-and-clear.json index 17cab3d4b6e..4d4fae94526 100644 --- a/mobile/rpc-foundation/goldens/terminal-gesture-flush-and-clear.json +++ b/mobile/rpc-foundation/goldens/terminal-gesture-flush-and-clear.json @@ -3,7 +3,7 @@ "family": "session.terminal-gesture-input", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "9d119d5ec320e2538105d6ff673b9f4b8e3decbe46527947489dffbcf2ac0472", diff --git a/mobile/rpc-foundation/goldens/terminal-input-send-accepted.json b/mobile/rpc-foundation/goldens/terminal-input-send-accepted.json index 5b31fb2815f..20b10bf9f51 100644 --- a/mobile/rpc-foundation/goldens/terminal-input-send-accepted.json +++ b/mobile/rpc-foundation/goldens/terminal-input-send-accepted.json @@ -3,7 +3,7 @@ "family": "session.terminal-input-send", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/terminal-input-send-refused.json b/mobile/rpc-foundation/goldens/terminal-input-send-refused.json index 235bf5d8fb1..6c79115b042 100644 --- a/mobile/rpc-foundation/goldens/terminal-input-send-refused.json +++ b/mobile/rpc-foundation/goldens/terminal-input-send-refused.json @@ -3,7 +3,7 @@ "family": "session.terminal-input-send", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/terminal-live-input-accepted.json b/mobile/rpc-foundation/goldens/terminal-live-input-accepted.json index bdf02eb377c..a5d891ad2da 100644 --- a/mobile/rpc-foundation/goldens/terminal-live-input-accepted.json +++ b/mobile/rpc-foundation/goldens/terminal-live-input-accepted.json @@ -3,7 +3,7 @@ "family": "session.terminal-input-send", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/terminal-paste-accepted.json b/mobile/rpc-foundation/goldens/terminal-paste-accepted.json index 272b3f51439..91c67bbc905 100644 --- a/mobile/rpc-foundation/goldens/terminal-paste-accepted.json +++ b/mobile/rpc-foundation/goldens/terminal-paste-accepted.json @@ -3,7 +3,7 @@ "family": "session.terminal-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/terminal-paste-refused.json b/mobile/rpc-foundation/goldens/terminal-paste-refused.json index 050bcbcef44..03437e393f1 100644 --- a/mobile/rpc-foundation/goldens/terminal-paste-refused.json +++ b/mobile/rpc-foundation/goldens/terminal-paste-refused.json @@ -3,7 +3,7 @@ "family": "session.terminal-paste", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/terminal-query-reply-accepted.json b/mobile/rpc-foundation/goldens/terminal-query-reply-accepted.json index 731587f7113..bbfe2e5f97a 100644 --- a/mobile/rpc-foundation/goldens/terminal-query-reply-accepted.json +++ b/mobile/rpc-foundation/goldens/terminal-query-reply-accepted.json @@ -3,7 +3,7 @@ "family": "terminal.query-reply", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/terminal-query-reply-unsubscribed.json b/mobile/rpc-foundation/goldens/terminal-query-reply-unsubscribed.json index 808684f321a..778fe54ab12 100644 --- a/mobile/rpc-foundation/goldens/terminal-query-reply-unsubscribed.json +++ b/mobile/rpc-foundation/goldens/terminal-query-reply-unsubscribed.json @@ -3,7 +3,7 @@ "family": "terminal.query-reply", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/terminal-raw-input-refused.json b/mobile/rpc-foundation/goldens/terminal-raw-input-refused.json index bab47e110d1..cffb8e0cebb 100644 --- a/mobile/rpc-foundation/goldens/terminal-raw-input-refused.json +++ b/mobile/rpc-foundation/goldens/terminal-raw-input-refused.json @@ -3,7 +3,7 @@ "family": "terminal.raw-input", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/terminal-raw-input-reported.json b/mobile/rpc-foundation/goldens/terminal-raw-input-reported.json index 1d55c4e56f1..982e02e225d 100644 --- a/mobile/rpc-foundation/goldens/terminal-raw-input-reported.json +++ b/mobile/rpc-foundation/goldens/terminal-raw-input-reported.json @@ -3,7 +3,7 @@ "family": "terminal.raw-input", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/terminal-takeover-report-accepted.json b/mobile/rpc-foundation/goldens/terminal-takeover-report-accepted.json index 5361ba46a99..0016f896946 100644 --- a/mobile/rpc-foundation/goldens/terminal-takeover-report-accepted.json +++ b/mobile/rpc-foundation/goldens/terminal-takeover-report-accepted.json @@ -3,7 +3,7 @@ "family": "terminal.takeover-report", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/terminal-takeover-report-retried.json b/mobile/rpc-foundation/goldens/terminal-takeover-report-retried.json index d3199ba5cfe..d9b8b3a635f 100644 --- a/mobile/rpc-foundation/goldens/terminal-takeover-report-retried.json +++ b/mobile/rpc-foundation/goldens/terminal-takeover-report-retried.json @@ -3,7 +3,7 @@ "family": "terminal.takeover-report", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/terminal-viewport-refit-applied.json b/mobile/rpc-foundation/goldens/terminal-viewport-refit-applied.json index 8169cb9e5d6..291fc99b141 100644 --- a/mobile/rpc-foundation/goldens/terminal-viewport-refit-applied.json +++ b/mobile/rpc-foundation/goldens/terminal-viewport-refit-applied.json @@ -3,7 +3,7 @@ "family": "terminal.viewport-refit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/terminal-viewport-refit-legacy-desktop.json b/mobile/rpc-foundation/goldens/terminal-viewport-refit-legacy-desktop.json index 84a26a94965..36f6d239604 100644 --- a/mobile/rpc-foundation/goldens/terminal-viewport-refit-legacy-desktop.json +++ b/mobile/rpc-foundation/goldens/terminal-viewport-refit-legacy-desktop.json @@ -3,7 +3,7 @@ "family": "terminal.viewport-refit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "7588d30f33a8bb846c48f160aa9a4a8138176662bb2fb6be7bbdf352f553d05f", diff --git a/mobile/rpc-foundation/goldens/terminal-worktree-connection-resolved.json b/mobile/rpc-foundation/goldens/terminal-worktree-connection-resolved.json index eefe8e653a3..a13462a4c1b 100644 --- a/mobile/rpc-foundation/goldens/terminal-worktree-connection-resolved.json +++ b/mobile/rpc-foundation/goldens/terminal-worktree-connection-resolved.json @@ -3,7 +3,7 @@ "family": "session.worktree-connection", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/tk-create-github.json b/mobile/rpc-foundation/goldens/tk-create-github.json index ae0e90a60b4..056be111f1f 100644 --- a/mobile/rpc-foundation/goldens/tk-create-github.json +++ b/mobile/rpc-foundation/goldens/tk-create-github.json @@ -3,7 +3,7 @@ "family": "tasks.task-create-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-create-gitlab.json b/mobile/rpc-foundation/goldens/tk-create-gitlab.json index 243534a05b3..f9c781b59cc 100644 --- a/mobile/rpc-foundation/goldens/tk-create-gitlab.json +++ b/mobile/rpc-foundation/goldens/tk-create-gitlab.json @@ -3,7 +3,7 @@ "family": "tasks.task-create-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-create-linear.json b/mobile/rpc-foundation/goldens/tk-create-linear.json index d4915627585..3c715ca7b0c 100644 --- a/mobile/rpc-foundation/goldens/tk-create-linear.json +++ b/mobile/rpc-foundation/goldens/tk-create-linear.json @@ -3,7 +3,7 @@ "family": "tasks.task-create-linear", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-item-checks-files.json b/mobile/rpc-foundation/goldens/tk-item-checks-files.json index d8cc425c451..e4f5ba3a006 100644 --- a/mobile/rpc-foundation/goldens/tk-item-checks-files.json +++ b/mobile/rpc-foundation/goldens/tk-item-checks-files.json @@ -3,7 +3,7 @@ "family": "tasks.item-checks-files", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/tk-item-comment-github.json b/mobile/rpc-foundation/goldens/tk-item-comment-github.json index 16eb40a8578..1a3196812ee 100644 --- a/mobile/rpc-foundation/goldens/tk-item-comment-github.json +++ b/mobile/rpc-foundation/goldens/tk-item-comment-github.json @@ -3,7 +3,7 @@ "family": "tasks.item-comment-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/tk-item-comment-gitlab-mr.json b/mobile/rpc-foundation/goldens/tk-item-comment-gitlab-mr.json index 86fea90e25b..e71ceb122ad 100644 --- a/mobile/rpc-foundation/goldens/tk-item-comment-gitlab-mr.json +++ b/mobile/rpc-foundation/goldens/tk-item-comment-gitlab-mr.json @@ -3,7 +3,7 @@ "family": "tasks.item-comment-gitlab-mr", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/tk-item-comment-gitlab.json b/mobile/rpc-foundation/goldens/tk-item-comment-gitlab.json index 9eadbcd9e81..04b99bf80ab 100644 --- a/mobile/rpc-foundation/goldens/tk-item-comment-gitlab.json +++ b/mobile/rpc-foundation/goldens/tk-item-comment-gitlab.json @@ -3,7 +3,7 @@ "family": "tasks.item-comment-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/tk-item-detail-github-reactions.json b/mobile/rpc-foundation/goldens/tk-item-detail-github-reactions.json index ee1b8a89283..0edc6709092 100644 --- a/mobile/rpc-foundation/goldens/tk-item-detail-github-reactions.json +++ b/mobile/rpc-foundation/goldens/tk-item-detail-github-reactions.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/tk-item-detail-github.json b/mobile/rpc-foundation/goldens/tk-item-detail-github.json index 79545254784..5923ba4bf1f 100644 --- a/mobile/rpc-foundation/goldens/tk-item-detail-github.json +++ b/mobile/rpc-foundation/goldens/tk-item-detail-github.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/tk-item-detail-gitlab-reactions.json b/mobile/rpc-foundation/goldens/tk-item-detail-gitlab-reactions.json index 7d3cabc8655..2979d93acfb 100644 --- a/mobile/rpc-foundation/goldens/tk-item-detail-gitlab-reactions.json +++ b/mobile/rpc-foundation/goldens/tk-item-detail-gitlab-reactions.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/tk-item-detail-gitlab.json b/mobile/rpc-foundation/goldens/tk-item-detail-gitlab.json index 7456862ac23..cc3ff056765 100644 --- a/mobile/rpc-foundation/goldens/tk-item-detail-gitlab.json +++ b/mobile/rpc-foundation/goldens/tk-item-detail-gitlab.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/tk-item-detail-linear.json b/mobile/rpc-foundation/goldens/tk-item-detail-linear.json index a499e51a0eb..15e6aba81bd 100644 --- a/mobile/rpc-foundation/goldens/tk-item-detail-linear.json +++ b/mobile/rpc-foundation/goldens/tk-item-detail-linear.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-linear", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c0ef16b959002e4a3c5347114a0844b95670e274ef010d910b6671ac5f49e783", diff --git a/mobile/rpc-foundation/goldens/tk-item-detail-metadata.json b/mobile/rpc-foundation/goldens/tk-item-detail-metadata.json index 7fb5d785a8d..2a499e70032 100644 --- a/mobile/rpc-foundation/goldens/tk-item-detail-metadata.json +++ b/mobile/rpc-foundation/goldens/tk-item-detail-metadata.json @@ -3,7 +3,7 @@ "family": "tasks.item-detail-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "58ea1553e04017c993aea4753aace41ee664705a3fdb3b18569c5a9d7968cf06", diff --git a/mobile/rpc-foundation/goldens/tk-item-merge-gitlab.json b/mobile/rpc-foundation/goldens/tk-item-merge-gitlab.json index 7e63fc0769b..6fd0875b846 100644 --- a/mobile/rpc-foundation/goldens/tk-item-merge-gitlab.json +++ b/mobile/rpc-foundation/goldens/tk-item-merge-gitlab.json @@ -3,7 +3,7 @@ "family": "tasks.item-merge-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/tk-item-metadata-github.json b/mobile/rpc-foundation/goldens/tk-item-metadata-github.json index 43838f19f02..e8d38bbb06e 100644 --- a/mobile/rpc-foundation/goldens/tk-item-metadata-github.json +++ b/mobile/rpc-foundation/goldens/tk-item-metadata-github.json @@ -3,7 +3,7 @@ "family": "tasks.item-metadata-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/tk-item-metadata-gitlab-mr.json b/mobile/rpc-foundation/goldens/tk-item-metadata-gitlab-mr.json index ad7b7d57786..8dff1ae8d34 100644 --- a/mobile/rpc-foundation/goldens/tk-item-metadata-gitlab-mr.json +++ b/mobile/rpc-foundation/goldens/tk-item-metadata-gitlab-mr.json @@ -3,7 +3,7 @@ "family": "tasks.item-metadata-gitlab-mr", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/tk-item-metadata-gitlab.json b/mobile/rpc-foundation/goldens/tk-item-metadata-gitlab.json index b13c5eaea50..db660a9866d 100644 --- a/mobile/rpc-foundation/goldens/tk-item-metadata-gitlab.json +++ b/mobile/rpc-foundation/goldens/tk-item-metadata-gitlab.json @@ -3,7 +3,7 @@ "family": "tasks.item-metadata-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/tk-item-reply-merge.json b/mobile/rpc-foundation/goldens/tk-item-reply-merge.json index 751f3787968..fae209129b1 100644 --- a/mobile/rpc-foundation/goldens/tk-item-reply-merge.json +++ b/mobile/rpc-foundation/goldens/tk-item-reply-merge.json @@ -3,7 +3,7 @@ "family": "tasks.item-reply-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/tk-item-review-github.json b/mobile/rpc-foundation/goldens/tk-item-review-github.json index 8c133390e85..57fd9af7473 100644 --- a/mobile/rpc-foundation/goldens/tk-item-review-github.json +++ b/mobile/rpc-foundation/goldens/tk-item-review-github.json @@ -3,7 +3,7 @@ "family": "tasks.item-review-github", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8f68885d57a9aa76d80ba0ee29a95bdbaa98cef29c79c68ce75d67202cde7bfe", diff --git a/mobile/rpc-foundation/goldens/tk-item-status-gitlab-mr.json b/mobile/rpc-foundation/goldens/tk-item-status-gitlab-mr.json index b6f0aacbfd9..b7cde92e644 100644 --- a/mobile/rpc-foundation/goldens/tk-item-status-gitlab-mr.json +++ b/mobile/rpc-foundation/goldens/tk-item-status-gitlab-mr.json @@ -3,7 +3,7 @@ "family": "tasks.item-status-gitlab-mr", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/tk-item-status-gitlab.json b/mobile/rpc-foundation/goldens/tk-item-status-gitlab.json index b0a2d3da129..d922a9114b2 100644 --- a/mobile/rpc-foundation/goldens/tk-item-status-gitlab.json +++ b/mobile/rpc-foundation/goldens/tk-item-status-gitlab.json @@ -3,7 +3,7 @@ "family": "tasks.item-status-gitlab", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/tk-linear-connect.json b/mobile/rpc-foundation/goldens/tk-linear-connect.json index 917a05af026..6d2936bd9f1 100644 --- a/mobile/rpc-foundation/goldens/tk-linear-connect.json +++ b/mobile/rpc-foundation/goldens/tk-linear-connect.json @@ -3,7 +3,7 @@ "family": "tasks.linear-connect", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-linear-item.json b/mobile/rpc-foundation/goldens/tk-linear-item.json index 853dea595cc..5e9ec14fed4 100644 --- a/mobile/rpc-foundation/goldens/tk-linear-item.json +++ b/mobile/rpc-foundation/goldens/tk-linear-item.json @@ -3,7 +3,7 @@ "family": "tasks.linear-item", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "97cfbcd82778ed6517ca2d10b2f3ad5a8d366e380d7846c1e89d5a5baf17e739", diff --git a/mobile/rpc-foundation/goldens/tk-linear-team-context.json b/mobile/rpc-foundation/goldens/tk-linear-team-context.json index 6e059219359..e81f0c4226a 100644 --- a/mobile/rpc-foundation/goldens/tk-linear-team-context.json +++ b/mobile/rpc-foundation/goldens/tk-linear-team-context.json @@ -3,7 +3,7 @@ "family": "tasks.linear-team-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "58ea1553e04017c993aea4753aace41ee664705a3fdb3b18569c5a9d7968cf06", diff --git a/mobile/rpc-foundation/goldens/tk-list-gitlab-items.json b/mobile/rpc-foundation/goldens/tk-list-gitlab-items.json index a357474a6ef..b9c4d68ec4e 100644 --- a/mobile/rpc-foundation/goldens/tk-list-gitlab-items.json +++ b/mobile/rpc-foundation/goldens/tk-list-gitlab-items.json @@ -3,7 +3,7 @@ "family": "tasks.task-list-gitlab-items", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-list-gitlab-todos.json b/mobile/rpc-foundation/goldens/tk-list-gitlab-todos.json index 59677e8af9e..16a274f76c7 100644 --- a/mobile/rpc-foundation/goldens/tk-list-gitlab-todos.json +++ b/mobile/rpc-foundation/goldens/tk-list-gitlab-todos.json @@ -3,7 +3,7 @@ "family": "tasks.task-list-gitlab-todos", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-list-linear.json b/mobile/rpc-foundation/goldens/tk-list-linear.json index 1a0982a4616..5d0ff05f9e3 100644 --- a/mobile/rpc-foundation/goldens/tk-list-linear.json +++ b/mobile/rpc-foundation/goldens/tk-list-linear.json @@ -3,7 +3,7 @@ "family": "tasks.task-list-linear", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-project-board-load.json b/mobile/rpc-foundation/goldens/tk-project-board-load.json index 09d70c53c32..ed74988dcdd 100644 --- a/mobile/rpc-foundation/goldens/tk-project-board-load.json +++ b/mobile/rpc-foundation/goldens/tk-project-board-load.json @@ -3,7 +3,7 @@ "family": "tasks.project-board-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c4272385ed3b0de4feab38de9e4f6363ecd6317fdd4de47f76a98eb18abaf371", diff --git a/mobile/rpc-foundation/goldens/tk-project-repo-slugs.json b/mobile/rpc-foundation/goldens/tk-project-repo-slugs.json index 187b93bd340..45fae56b77c 100644 --- a/mobile/rpc-foundation/goldens/tk-project-repo-slugs.json +++ b/mobile/rpc-foundation/goldens/tk-project-repo-slugs.json @@ -3,7 +3,7 @@ "family": "tasks.project-repo-slugs", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "c4272385ed3b0de4feab38de9e4f6363ecd6317fdd4de47f76a98eb18abaf371", diff --git a/mobile/rpc-foundation/goldens/tk-project-row-comments-issue.json b/mobile/rpc-foundation/goldens/tk-project-row-comments-issue.json index 9734b1b192c..f7e9f7c3ee0 100644 --- a/mobile/rpc-foundation/goldens/tk-project-row-comments-issue.json +++ b/mobile/rpc-foundation/goldens/tk-project-row-comments-issue.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-comments-issue", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/tk-project-row-comments-pr.json b/mobile/rpc-foundation/goldens/tk-project-row-comments-pr.json index 26a1e023877..690f9b33360 100644 --- a/mobile/rpc-foundation/goldens/tk-project-row-comments-pr.json +++ b/mobile/rpc-foundation/goldens/tk-project-row-comments-pr.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-comments-pr", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/tk-project-row-detail.json b/mobile/rpc-foundation/goldens/tk-project-row-detail.json index 7ba4700f58b..49e5913b0f9 100644 --- a/mobile/rpc-foundation/goldens/tk-project-row-detail.json +++ b/mobile/rpc-foundation/goldens/tk-project-row-detail.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-detail", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f8f6e5d500f959b9b15c5498885a05422747880b6aef4ad795bc3064ebbacea6", diff --git a/mobile/rpc-foundation/goldens/tk-project-row-fields.json b/mobile/rpc-foundation/goldens/tk-project-row-fields.json index cc90b1e5c69..a3bb0476de5 100644 --- a/mobile/rpc-foundation/goldens/tk-project-row-fields.json +++ b/mobile/rpc-foundation/goldens/tk-project-row-fields.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-fields", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/tk-project-row-files-merge.json b/mobile/rpc-foundation/goldens/tk-project-row-files-merge.json index 933c00d6683..018c5975f31 100644 --- a/mobile/rpc-foundation/goldens/tk-project-row-files-merge.json +++ b/mobile/rpc-foundation/goldens/tk-project-row-files-merge.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-files-merge", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "b228732762828412ad3d9eec3ece00a897d866046e37044322c3911758d6e0a9", diff --git a/mobile/rpc-foundation/goldens/tk-project-row-metadata-load.json b/mobile/rpc-foundation/goldens/tk-project-row-metadata-load.json index 8b3d11354d3..94b92d97f50 100644 --- a/mobile/rpc-foundation/goldens/tk-project-row-metadata-load.json +++ b/mobile/rpc-foundation/goldens/tk-project-row-metadata-load.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-metadata-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "f8f6e5d500f959b9b15c5498885a05422747880b6aef4ad795bc3064ebbacea6", diff --git a/mobile/rpc-foundation/goldens/tk-project-row-review-checks.json b/mobile/rpc-foundation/goldens/tk-project-row-review-checks.json index 4f6440307e7..7c2627b8619 100644 --- a/mobile/rpc-foundation/goldens/tk-project-row-review-checks.json +++ b/mobile/rpc-foundation/goldens/tk-project-row-review-checks.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-review-checks", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "370aeaee59978071ccb821da13c9e6114936c168947b608539cdb80d40cc9889", diff --git a/mobile/rpc-foundation/goldens/tk-project-row-threads.json b/mobile/rpc-foundation/goldens/tk-project-row-threads.json index 48860ada04c..a6a234eea3b 100644 --- a/mobile/rpc-foundation/goldens/tk-project-row-threads.json +++ b/mobile/rpc-foundation/goldens/tk-project-row-threads.json @@ -3,7 +3,7 @@ "family": "tasks.project-row-threads", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/tk-provider-load.json b/mobile/rpc-foundation/goldens/tk-provider-load.json index 40fd65f3a6c..c60c07a063c 100644 --- a/mobile/rpc-foundation/goldens/tk-provider-load.json +++ b/mobile/rpc-foundation/goldens/tk-provider-load.json @@ -3,7 +3,7 @@ "family": "tasks.provider-load", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/transport-capability-probe-cutover-reasks-fast.json b/mobile/rpc-foundation/goldens/transport-capability-probe-cutover-reasks-fast.json index 8dc2d37229c..d18e4f70910 100644 --- a/mobile/rpc-foundation/goldens/transport-capability-probe-cutover-reasks-fast.json +++ b/mobile/rpc-foundation/goldens/transport-capability-probe-cutover-reasks-fast.json @@ -3,7 +3,7 @@ "family": "transport.capability-probe", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-capability-probe-non-string-capabilities-drop.json b/mobile/rpc-foundation/goldens/transport-capability-probe-non-string-capabilities-drop.json index 65fc3816637..ab1afc2965d 100644 --- a/mobile/rpc-foundation/goldens/transport-capability-probe-non-string-capabilities-drop.json +++ b/mobile/rpc-foundation/goldens/transport-capability-probe-non-string-capabilities-drop.json @@ -3,7 +3,7 @@ "family": "transport.capability-probe", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-capability-probe-publishes.json b/mobile/rpc-foundation/goldens/transport-capability-probe-publishes.json index cbbc6737dbe..6d31cb48ba9 100644 --- a/mobile/rpc-foundation/goldens/transport-capability-probe-publishes.json +++ b/mobile/rpc-foundation/goldens/transport-capability-probe-publishes.json @@ -3,7 +3,7 @@ "family": "transport.capability-probe", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-capability-probe-refused-backs-off.json b/mobile/rpc-foundation/goldens/transport-capability-probe-refused-backs-off.json index e4b77987fb6..bf4ebf9a20b 100644 --- a/mobile/rpc-foundation/goldens/transport-capability-probe-refused-backs-off.json +++ b/mobile/rpc-foundation/goldens/transport-capability-probe-refused-backs-off.json @@ -3,7 +3,7 @@ "family": "transport.capability-probe", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-host-status-gates-drop-keeps-capabilities.json b/mobile/rpc-foundation/goldens/transport-host-status-gates-drop-keeps-capabilities.json index eb62ab108d3..3787c1d6c15 100644 --- a/mobile/rpc-foundation/goldens/transport-host-status-gates-drop-keeps-capabilities.json +++ b/mobile/rpc-foundation/goldens/transport-host-status-gates-drop-keeps-capabilities.json @@ -3,7 +3,7 @@ "family": "transport.host-status-gates", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-host-status-gates-ready.json b/mobile/rpc-foundation/goldens/transport-host-status-gates-ready.json index 49d853418fb..f6b7263c5b4 100644 --- a/mobile/rpc-foundation/goldens/transport-host-status-gates-ready.json +++ b/mobile/rpc-foundation/goldens/transport-host-status-gates-ready.json @@ -3,7 +3,7 @@ "family": "transport.host-status-gates", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-host-status-gates-refused-degrades.json b/mobile/rpc-foundation/goldens/transport-host-status-gates-refused-degrades.json index 84a3ec14056..dac18e8dfee 100644 --- a/mobile/rpc-foundation/goldens/transport-host-status-gates-refused-degrades.json +++ b/mobile/rpc-foundation/goldens/transport-host-status-gates-refused-degrades.json @@ -3,7 +3,7 @@ "family": "transport.host-status-gates", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-pairing-race-both-refused.json b/mobile/rpc-foundation/goldens/transport-pairing-race-both-refused.json index fe698a7d03b..7cd051f2b0b 100644 --- a/mobile/rpc-foundation/goldens/transport-pairing-race-both-refused.json +++ b/mobile/rpc-foundation/goldens/transport-pairing-race-both-refused.json @@ -3,7 +3,7 @@ "family": "transport.pairing-race", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-pairing-race-direct-completes-first.json b/mobile/rpc-foundation/goldens/transport-pairing-race-direct-completes-first.json index 74c86d12c6a..d46784c8394 100644 --- a/mobile/rpc-foundation/goldens/transport-pairing-race-direct-completes-first.json +++ b/mobile/rpc-foundation/goldens/transport-pairing-race-direct-completes-first.json @@ -3,7 +3,7 @@ "family": "transport.pairing-race", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-pairing-race-relay-completes-first.json b/mobile/rpc-foundation/goldens/transport-pairing-race-relay-completes-first.json index 9f52840746b..fe52b9266b1 100644 --- a/mobile/rpc-foundation/goldens/transport-pairing-race-relay-completes-first.json +++ b/mobile/rpc-foundation/goldens/transport-pairing-race-relay-completes-first.json @@ -3,7 +3,7 @@ "family": "transport.pairing-race", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/transport-pairing-race-relay-wins-when-direct-refused.json b/mobile/rpc-foundation/goldens/transport-pairing-race-relay-wins-when-direct-refused.json index 833efdd49d9..8198d6343ae 100644 --- a/mobile/rpc-foundation/goldens/transport-pairing-race-relay-wins-when-direct-refused.json +++ b/mobile/rpc-foundation/goldens/transport-pairing-race-relay-wins-when-direct-refused.json @@ -3,7 +3,7 @@ "family": "transport.pairing-race", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json b/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json index 005392b53c6..980e2d18517 100644 --- a/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json +++ b/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json @@ -3,7 +3,7 @@ "family": "worktree.runtime-capabilities", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json b/mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json index 71c33439e52..0bb5e8c6291 100644 --- a/mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json +++ b/mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json @@ -3,7 +3,7 @@ "family": "worktree.runtime-capabilities", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json b/mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json index 9fd619c4074..cd2e6851aa8 100644 --- a/mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json +++ b/mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json @@ -3,7 +3,7 @@ "family": "worktree.runtime-capabilities", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-agent-launched.json b/mobile/rpc-foundation/goldens/tw-create-retry-agent-launched.json index c38a625b3ed..ec9f4ca6a09 100644 --- a/mobile/rpc-foundation/goldens/tw-create-retry-agent-launched.json +++ b/mobile/rpc-foundation/goldens/tw-create-retry-agent-launched.json @@ -3,7 +3,7 @@ "family": "worktree.agent-launch-create", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json index 504da80c6a0..64329f03ba6 100644 --- a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json +++ b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json @@ -3,7 +3,7 @@ "family": "worktree.create-retry", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json index 0741a4ba24b..f94b3aa50b7 100644 --- a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json +++ b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json @@ -3,7 +3,7 @@ "family": "worktree.create-retry", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json index 19497ae72c9..cea4816faab 100644 --- a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json +++ b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json @@ -3,7 +3,7 @@ "family": "worktree.create-retry", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-created.json b/mobile/rpc-foundation/goldens/tw-create-retry-created.json index f5eda2bce88..2ad976ac03d 100644 --- a/mobile/rpc-foundation/goldens/tw-create-retry-created.json +++ b/mobile/rpc-foundation/goldens/tw-create-retry-created.json @@ -3,7 +3,7 @@ "family": "worktree.create-retry", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json b/mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json index c16c16b395f..d56fb59f848 100644 --- a/mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json +++ b/mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json @@ -3,7 +3,7 @@ "family": "worktree.create-retry", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json b/mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json index 0817c4d7d29..bdf3a62334d 100644 --- a/mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json +++ b/mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json @@ -3,7 +3,7 @@ "family": "worktree.create-retry", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json b/mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json index ae93a58f838..47d93d1fffa 100644 --- a/mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json +++ b/mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json @@ -3,7 +3,7 @@ "family": "worktree.create-retry", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json b/mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json index fda4c1d2147..c67998503f3 100644 --- a/mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json +++ b/mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json @@ -3,7 +3,7 @@ "family": "worktree.hosted-base", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json b/mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json index e0dfd184ad0..28f32fd2b42 100644 --- a/mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json +++ b/mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json @@ -3,7 +3,7 @@ "family": "worktree.hosted-base", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json b/mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json index a394d20ff47..3cd6698ae60 100644 --- a/mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json +++ b/mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json @@ -3,7 +3,7 @@ "family": "tasks.paste-lookup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json b/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json index a3bafc37f69..a1ae250a957 100644 --- a/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json +++ b/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json @@ -3,7 +3,7 @@ "family": "tasks.paste-lookup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json b/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json index 890199612c6..bdbb6ef710f 100644 --- a/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json +++ b/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json @@ -3,7 +3,7 @@ "family": "tasks.paste-lookup", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json b/mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json index e4aba088ecf..2b52b990d95 100644 --- a/mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json +++ b/mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json @@ -3,7 +3,7 @@ "family": "worktree.setup-hook-trust", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json b/mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json index 578f6c009d9..acb19a7c0cf 100644 --- a/mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json +++ b/mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json @@ -3,7 +3,7 @@ "family": "worktree.setup-hook-trust", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json b/mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json index ad774256225..9e35e07e804 100644 --- a/mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json +++ b/mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json @@ -3,7 +3,7 @@ "family": "tasks.smart-source-search", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json b/mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json index 643ac585eec..7bdd56a1f96 100644 --- a/mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json +++ b/mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json @@ -3,7 +3,7 @@ "family": "tasks.smart-source-search", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json b/mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json index 4af6641b24a..6d94a4e7b96 100644 --- a/mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json +++ b/mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json @@ -3,7 +3,7 @@ "family": "tasks.smart-source-search", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "52a76b7a830b32287bce14abbe1b9d9ac70e71eafe5b4c6801c2eb14a4150125", diff --git a/mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json b/mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json index b2f3dc41be0..779c8c60a1a 100644 --- a/mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json +++ b/mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json @@ -3,7 +3,7 @@ "family": "settings-best-effort", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json b/mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json index 96435ebc76d..d2cb946fd8d 100644 --- a/mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json +++ b/mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-source", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/tw-workspace-source-presets.json b/mobile/rpc-foundation/goldens/tw-workspace-source-presets.json index 0670fa77476..6cbe4aaa513 100644 --- a/mobile/rpc-foundation/goldens/tw-workspace-source-presets.json +++ b/mobile/rpc-foundation/goldens/tw-workspace-source-presets.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-source", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json b/mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json index 3877213a8c3..4d90a6fac9c 100644 --- a/mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json +++ b/mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-sparse", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json b/mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json index 76f26fcd80c..1fcaf981e87 100644 --- a/mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json +++ b/mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-sparse", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json b/mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json index d4124bd9e64..10d5cf3612c 100644 --- a/mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json +++ b/mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-ssh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json b/mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json index 7e18f38a1c5..2013e441497 100644 --- a/mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json +++ b/mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-ssh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json b/mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json index 072c5a1103a..41f0d873dd2 100644 --- a/mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json +++ b/mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-ssh-local", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json b/mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json index 159b10c78ce..5e33d63d533 100644 --- a/mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json +++ b/mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json @@ -3,7 +3,7 @@ "family": "tasks.workspace-ssh", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", diff --git a/mobile/rpc-foundation/goldens/worktree-catalog-snapshot-unreadable.json b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot-unreadable.json index 8f3a28547eb..01a477cc887 100644 --- a/mobile/rpc-foundation/goldens/worktree-catalog-snapshot-unreadable.json +++ b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot-unreadable.json @@ -3,7 +3,7 @@ "family": "worktree.catalog-snapshot", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json index c1ac6642467..295ddfad359 100644 --- a/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json +++ b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json @@ -3,7 +3,7 @@ "family": "worktree.catalog-snapshot", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/worktree-home-catalog.json b/mobile/rpc-foundation/goldens/worktree-home-catalog.json index 22bb0ceee22..efeba0e1e19 100644 --- a/mobile/rpc-foundation/goldens/worktree-home-catalog.json +++ b/mobile/rpc-foundation/goldens/worktree-home-catalog.json @@ -3,7 +3,7 @@ "family": "worktree.home-catalog", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/worktree-retired-names.json b/mobile/rpc-foundation/goldens/worktree-retired-names.json index 676c6e15c34..562e03f3840 100644 --- a/mobile/rpc-foundation/goldens/worktree-retired-names.json +++ b/mobile/rpc-foundation/goldens/worktree-retired-names.json @@ -3,7 +3,7 @@ "family": "worktree.retired-names", "namedDeltas": [], "runnerVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "lockfileSha256": "4568633356187895ccda8e2ff83245c37d5621832c69e268733733b6947971e3", "recorderSha256": "1d8cf6624653d15b5b32bd2871d4d93739d4092a06f10dab24a795702422bb31", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/pilot-scenarios.json b/mobile/rpc-foundation/pilot-scenarios.json index 067c4073ba4..db5a78e7eab 100644 --- a/mobile/rpc-foundation/pilot-scenarios.json +++ b/mobile/rpc-foundation/pilot-scenarios.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "baseline": "6142657d7ad4af81cb4449176048edadef10db45", + "baseline": "5741d48e592dad21660d995a3758319413a4baf0", "scenarios": [ { "id": "b1", @@ -1178,6 +1178,118 @@ } ] }, + { + "id": "settings-repo-metadata-icons", + "operation": "settings.repo-metadata", + "version": 1, + "family": "settings.repo-metadata", + "sites": ["mobile/src/host-screen/use-host-repo-metadata.ts"], + "schedules": ["settings-first"], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "action": "load", + "id": "load" + }, + { + "complete": "repo.list#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "repos": [ + { + "id": "repo-1", + "displayName": "Lucide", + "badgeColor": "#123456", + "repoIcon": { + "type": "lucide", + "name": "Rocket" + }, + "connectionId": null + }, + { + "id": "repo-2", + "displayName": "Emoji", + "repoIcon": { + "type": "emoji", + "emoji": "🐳" + }, + "connectionId": "ssh-1" + }, + { + "id": "repo-3", + "displayName": "Avatar", + "repoIcon": { + "type": "image", + "src": "https://github.com/acme.png?size=64", + "source": "github", + "label": "acme/orca" + }, + "executionHostId": "ssh:ssh-1" + } + ] + } + } + }, + { + "complete": "ssh.listTargetSummaries#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "targets": [ + { + "id": "ssh-1", + "label": "SSH" + } + ] + } + } + }, + { + "checkpoint": "settings-pending" + }, + { + "complete": "settings.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [], + "hostSettingOverrides": {} + } + } + } + }, + { + "complete": "host.platform#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "platform": "darwin" + } + } + }, + { + "checkpoint": "settled" + } + ] + }, { "id": "settings-repo-metadata-refused", "operation": "settings.repo-metadata", @@ -14039,6 +14151,55 @@ } ] }, + { + "id": "speech-setup-sheet-model-vocabulary", + "operation": "speech.setup-sheet", + "version": 1, + "family": "speech.setup-sheet", + "sites": ["mobile/src/dictation/mobile-dictation-setup.ts"], + "schedules": [], + "steps": [ + { + "action": "list", + "id": "list" + }, + { + "complete": "speech.models.list#1", + "params": null, + "reply": { + "ok": true, + "result": { + "enabled": true, + "selectedModelId": "gpt-4o-transcribe", + "dictationMode": "hold", + "models": [ + { + "id": "whisper-small", + "label": "Small", + "provider": "local", + "status": "downloading", + "sizeBytes": 466862080, + "progress": 0.42, + "recommended": true + }, + { + "id": "gpt-4o-transcribe", + "label": "OpenAI", + "provider": "openai", + "status": "ready", + "sizeBytes": null, + "progress": null, + "recommended": false + } + ] + } + } + }, + { + "checkpoint": "settled" + } + ] + }, { "id": "speech-setup-sheet-legacy-desktop", "operation": "speech.setup-sheet", @@ -22935,6 +23096,517 @@ "checkpoint": "settled" } ] + }, + { + "id": "files-preview-worktree-text-read", + "operation": "files.preview-load", + "version": 1, + "family": "files.preview-worktree-text", + "sites": ["mobile/src/files/mobile-file-preview-request.ts"], + "schedules": [], + "steps": [ + { + "action": "worktree", + "id": "load" + }, + { + "complete": "files.read#1", + "params": { + "worktree": "id:workspace-1", + "relativePath": "docs/readme.md" + }, + "reply": { + "ok": true, + "result": { + "content": "# readme", + "truncated": false, + "byteLength": 8 + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-preview-worktree-image-read", + "operation": "files.preview-load", + "version": 1, + "family": "files.preview-worktree-image", + "sites": ["mobile/src/files/mobile-file-preview-request.ts"], + "schedules": [], + "steps": [ + { + "action": "worktree", + "id": "load", + "args": { + "path": "docs/logo.png" + } + }, + { + "complete": "files.readPreview#1", + "params": { + "worktree": "id:workspace-1", + "relativePath": "docs/logo.png" + }, + "reply": { + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-preview-artifact-image-read", + "operation": "files.preview-load", + "version": 1, + "family": "files.preview-artifact-image", + "sites": ["mobile/src/files/mobile-file-preview-request.ts"], + "schedules": [], + "steps": [ + { + "action": "artifact", + "id": "load", + "args": { + "path": "/logs/shot.png" + } + }, + { + "complete": "files.readTerminalArtifactPreview#1", + "params": { + "worktree": "id:workspace-1", + "absolutePath": "/logs/shot.png", + "grantId": "grant-1" + }, + "reply": { + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "session-markdown-disk-read", + "operation": "session.tab-documents", + "version": 1, + "family": "session.markdown-disk-fallback", + "sites": ["mobile/src/session/use-mobile-session-document-readers.ts"], + "schedules": [], + "steps": [ + { + "action": "markdown", + "id": "markdown" + }, + { + "complete": "markdown.readTab#1", + "params": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + }, + "reply": { + "ok": false, + "error": { + "code": "renderer_unavailable", + "message": "Renderer unavailable" + } + } + }, + { + "complete": "files.read#1", + "params": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + }, + "reply": { + "ok": true, + "result": { + "content": "# disk", + "truncated": false, + "byteLength": 6 + } + } + }, + { + "checkpoint": "fell-back" + } + ] + }, + { + "id": "session-markdown-disk-served", + "operation": "session.tab-documents", + "version": 1, + "family": "session.markdown-disk-fallback", + "sites": ["mobile/src/session/use-mobile-session-document-readers.ts"], + "schedules": [], + "steps": [ + { + "action": "markdown", + "id": "markdown" + }, + { + "complete": "markdown.readTab#1", + "params": { + "tabId": "tab-md", + "worktree": "id:workspace-1" + }, + "reply": { + "ok": true, + "result": { + "content": "# served", + "version": "v1", + "isDirty": false, + "editable": true + } + } + }, + { + "checkpoint": "served" + } + ] + }, + { + "id": "mobile-web-bundle-manifest-read", + "operation": "mobileWeb.bundle-manifest", + "version": 1, + "family": "mobileWeb.bundle-manifest", + "sites": ["mobile/src/transport/mobile-web-bundle-operations.ts"], + "schedules": [], + "steps": [ + { + "action": "read", + "id": "read" + }, + { + "complete": "mobileWeb.bundle.manifest#1", + "params": null, + "reply": { + "ok": true, + "result": { + "manifest": { + "schemaVersion": 1, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "entrypoint": "index.html", + "totalBytes": 37, + "assets": [ + { + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d", + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8" + }, + { + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e", + "byteLength": 26, + "contentType": "text/html; charset=utf-8" + } + ] + }, + "chunkBytes": 16 + } + } + }, + { + "checkpoint": "manifest-read" + } + ] + }, + { + "id": "mobile-web-bundle-fetch-paged", + "operation": "mobileWeb.bundle-fetch", + "version": 1, + "family": "mobileWeb.bundle-fetch", + "sites": [ + "mobile/src/transport/mobile-web-bundle-fetch.ts", + "mobile/src/diagnostics/use-mobile-web-bundle-probe.ts" + ], + "schedules": [], + "steps": [ + { + "action": "fetch", + "id": "fetch" + }, + { + "complete": "mobileWeb.bundle.manifest#1", + "params": null, + "reply": { + "ok": true, + "result": { + "manifest": { + "schemaVersion": 1, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "entrypoint": "index.html", + "totalBytes": 37, + "assets": [ + { + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d", + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8" + }, + { + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e", + "byteLength": 26, + "contentType": "text/html; charset=utf-8" + } + ] + }, + "chunkBytes": 16 + } + } + }, + { + "bind": "app-js", + "request": "mobileWeb.bundle.chunk#1", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "assets/app.js", + "offset": 0 + } + }, + { + "bind": "index-head", + "request": "mobileWeb.bundle.chunk#2", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "index.html", + "offset": 0 + } + }, + { + "complete": "app-js", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "assets/app.js", + "offset": 0 + }, + "reply": { + "ok": true, + "result": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "assets/app.js", + "offset": 0, + "assetByteLength": 11, + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d", + "dataBase64": "b3JjYS5ib290KCk=", + "eof": true + } + } + }, + { + "complete": "index-head", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "index.html", + "offset": 0 + }, + "reply": { + "ok": true, + "result": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "index.html", + "offset": 0, + "assetByteLength": 26, + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e", + "dataBase64": "PCFkb2N0eXBlIGh0bWw+PA==", + "eof": false + } + } + }, + { + "bind": "index-tail", + "request": "mobileWeb.bundle.chunk#3", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "index.html", + "offset": 16 + } + }, + { + "complete": "index-tail", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "index.html", + "offset": 16 + }, + "reply": { + "ok": true, + "result": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "index.html", + "offset": 16, + "assetByteLength": 26, + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e", + "dataBase64": "cD5vcmNhPC9wPg==", + "eof": true + } + } + }, + { + "checkpoint": "bundle-fetched" + } + ] + }, + { + "id": "mobile-web-bundle-unavailable", + "operation": "mobileWeb.bundle-fetch", + "version": 1, + "family": "mobileWeb.bundle-fetch", + "sites": [ + "mobile/src/transport/mobile-web-bundle-fetch.ts", + "mobile/src/diagnostics/use-mobile-web-bundle-probe.ts" + ], + "schedules": [], + "steps": [ + { + "action": "fetch", + "id": "fetch" + }, + { + "complete": "mobileWeb.bundle.manifest#1", + "params": null, + "reply": { + "ok": false, + "error": { + "code": "invalid_argument", + "message": "mobile_web_bundle_unavailable" + } + } + }, + { + "checkpoint": "bundle-unavailable" + } + ] + }, + { + "id": "mobile-web-bundle-build-changed", + "operation": "mobileWeb.bundle-fetch", + "version": 1, + "family": "mobileWeb.bundle-fetch", + "sites": [ + "mobile/src/transport/mobile-web-bundle-fetch.ts", + "mobile/src/diagnostics/use-mobile-web-bundle-probe.ts" + ], + "schedules": [], + "steps": [ + { + "action": "fetch", + "id": "fetch" + }, + { + "complete": "mobileWeb.bundle.manifest#1", + "params": null, + "reply": { + "ok": true, + "result": { + "manifest": { + "schemaVersion": 1, + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "desktopVersion": "1.4.201", + "minCompatibleRuntimeProtocolVersion": 2, + "runtimeProtocolVersion": 2, + "entrypoint": "index.html", + "totalBytes": 37, + "assets": [ + { + "path": "assets/app.js", + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d", + "byteLength": 11, + "contentType": "text/javascript; charset=utf-8" + }, + { + "path": "index.html", + "sha256": "483f915496f213c851665840f49b69e05b7a6bf70ec9d6939a831b15d298f31e", + "byteLength": 26, + "contentType": "text/html; charset=utf-8" + } + ] + }, + "chunkBytes": 16 + } + } + }, + { + "bind": "app-js", + "request": "mobileWeb.bundle.chunk#1", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "assets/app.js", + "offset": 0 + } + }, + { + "bind": "index-head", + "request": "mobileWeb.bundle.chunk#2", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "index.html", + "offset": 0 + } + }, + { + "complete": "app-js", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "assets/app.js", + "offset": 0 + }, + "reply": { + "ok": true, + "result": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "assets/app.js", + "offset": 0, + "assetByteLength": 11, + "sha256": "e99170780c392398db81fbb3dcaebc1a2c8264d4d8a9cd5932887e9c7206dc3d", + "dataBase64": "b3JjYS5ib290KCk=", + "eof": true + } + } + }, + { + "complete": "index-head", + "params": { + "buildId": "973c008f6baf56072d8d3d41f451e005257b59026401473543ce96e81c5aa177", + "path": "index.html", + "offset": 0 + }, + "reply": { + "ok": false, + "error": { + "code": "invalid_argument", + "message": "mobile_web_bundle_build_changed" + } + } + }, + { + "checkpoint": "bundle-build-changed" + } + ] } ] } diff --git a/mobile/scripts/check-tests-typecheck-ratchet.mjs b/mobile/scripts/check-tests-typecheck-ratchet.mjs new file mode 100644 index 00000000000..f82e5c1a278 --- /dev/null +++ b/mobile/scripts/check-tests-typecheck-ratchet.mjs @@ -0,0 +1,347 @@ +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' + +// Ratchet gate for the mobile test typecheck. +// +// mobile/tsconfig.json excludes *.test.ts so Metro never compiles tests into the release bundle, +// and vitest transpiles without typechecking. Nothing checked a mobile test until tsconfig.test.json +// existed, so 144 of the 632 test files had accumulated type errors — overwhelmingly one seam, the +// react-test-renderer / mocked-react-native pair, whose fix is a test-support typing decision rather +// than 587 local edits. This check freezes that set and fails when a test file that typechecks today +// stops doing so. The baseline may only shrink. + +const BASELINE_PATH = 'tests-typecheck-baseline.txt' +const PROJECT = 'tsconfig.test.json' +const ERROR_LINE = /^(\S.*?)\(\d+,\d+\): error TS\d+:/ + +// The only test files allowed to sit outside the program, and why. Everything else on disk must be +// in it: the error diff below sees a file only once it errors, so an excluded or shadowed test +// disappears from this gate silently. +export const TESTS_OUTSIDE_PROGRAM = new Map([ + [ + 'scripts/rpc-recording-pin-guard.test.ts', + 'Node-side: imports the desktop main process, checked against @types/node rather than RN libs' + ], + [ + 'src/tasks/agent-launch-mobile-replay.test.ts', + 'Node-side: imports the desktop main process, checked against @types/node rather than RN libs' + ], + [ + 'src/tasks/mobile-agent-launch-architecture.test.ts', + 'Node-side: imports the desktop main process, checked against @types/node rather than RN libs' + ], + [ + 'src/transport/mobile-relay-browser-cancel-budget.test.ts', + 'Node-side: imports src/shared/child-process, checked against @types/node rather than RN libs' + ] +]) + +// tsc prints the host's own separator; the baseline stores POSIX, so a Windows run would otherwise +// read every entry as both stale and added. +const toPosix = (filePath) => filePath.replaceAll('\\', '/') + +export function parseFailingFiles(tscOutput) { + const files = new Set() + for (const line of tscOutput.split('\n')) { + const matched = ERROR_LINE.exec(line) + if (matched) { + files.add(toPosix(matched[1])) + } + } + return [...files].sort() +} + +export function collectTestFilesOnDisk(root = process.cwd()) { + const found = [] + const walk = (dir) => { + for (const entry of fs.readdirSync(path.join(root, dir), { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name.startsWith('.')) { + continue + } + const rel = dir ? `${dir}/${entry.name}` : entry.name + if (entry.isDirectory()) { + walk(rel) + } else if (/\.test\.tsx?$/.test(entry.name)) { + found.push(rel) + } + } + } + walk('') + return found.sort() +} + +// --listFiles prints one absolute real path per line into the same stream as the diagnostics; a +// diagnostic carries `(line,col): error` and a path relative to cwd, so neither filter can take the +// other's lines. +export function parseProgramTestFiles(tscOutput, realRoot) { + const prefix = `${toPosix(realRoot).replace(/\/$/, '')}/` + return [ + ...new Set( + tscOutput + .split('\n') + .map((line) => toPosix(line.trim())) + .filter((line) => /\.test\.tsx?$/.test(line) && line.startsWith(prefix)) + .map((line) => line.slice(prefix.length)) + ) + ].sort() +} + +// A baselined test could otherwise be "fixed" with one `@ts-nocheck`, pruned, and never checked +// again: tsc exits 0 on such a file and nothing else here would notice. +export function hasTsNocheckDirective(source) { + for (const line of source.split('\n')) { + const text = line.trim() + if (text === '') { + continue + } + if (!text.startsWith('//') && !text.startsWith('/*') && !text.startsWith('*')) { + return false + } + if (text.includes('@ts-nocheck')) { + return true + } + } + return false +} + +export function findTsNocheckFiles(root, files) { + return files + .filter((file) => hasTsNocheckDirective(fs.readFileSync(path.join(root, file), 'utf8'))) + .sort() +} + +export function diffCensus(onDisk, inProgram, allowed = TESTS_OUTSIDE_PROGRAM) { + const program = new Set(inProgram) + const allow = allowed instanceof Map ? allowed : new Map(allowed.map((e) => [e, ''])) + const disk = new Set(onDisk) + return { + missing: [...disk].filter((entry) => !program.has(entry) && !allow.has(entry)).sort(), + staleAllowance: [...allow.keys()] + .filter((entry) => !disk.has(entry) || program.has(entry)) + .sort() + } +} + +export function parseBaseline(text) { + return new Set( + text + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + ) +} + +export function diffBaseline(current, baseline) { + const cur = new Set(current) + const base = baseline instanceof Set ? baseline : new Set(baseline) + return { + added: [...cur].filter((entry) => !base.has(entry)).sort(), + stale: [...base].filter((entry) => !cur.has(entry)).sort() + } +} + +// Run tsc's JS entry on this Node rather than the node_modules/.bin shim, which is a POSIX shell +// script: on Windows the shim is tsc.CMD and an extensionless path gets .exe appended. +function runTsc(root, args) { + const entry = createRequire(import.meta.url).resolve('typescript/lib/tsc.js') + const result = spawnSync(process.execPath, [entry, ...args], { + cwd: root, + encoding: 'utf8', + shell: false, + maxBuffer: 64 * 1024 * 1024 + }) + if (result.error) { + throw result.error + } + return result +} + +// tsc exits non-zero on type errors, which is the expected state here, so only a crash is fatal. +// One pass answers both questions: --listFiles names the program, the diagnostics name the failures. +export function collectTypecheckPass(root = process.cwd()) { + const result = runTsc(root, ['--noEmit', '--listFiles', '-p', PROJECT]) + const output = `${result.stdout ?? ''}${result.stderr ?? ''}` + return { + failing: parseFailingFiles(output), + programTestFiles: parseProgramTestFiles(output, fs.realpathSync(root)) + } +} + +export function collectCurrentFailingFiles(root = process.cwd()) { + return collectTypecheckPass(root).failing +} + +function printAddedFailure(added) { + for (const entry of added) { + console.error(`::error::Test file no longer typechecks: ${entry}`) + } + console.error('') + console.error('╭────────────────────────────────────────────────────────────────────────────╮') + console.error('│ ❌ mobile tests typecheck ratchet failed — a test file stopped checking. │') + console.error('╰────────────────────────────────────────────────────────────────────────────╯') + console.error('') + console.error(` ${added.length} test file(s) newly fail \`tsc -p ${PROJECT}\`:`) + console.error('') + for (const entry of added) { + console.error(` • ${entry}`) + } + console.error('') + console.error(' See the errors with: pnpm --filter orca-mobile typecheck:tests') + console.error('') + console.error(' A type-level pin in an unchecked test proves nothing, which is the whole reason') + console.error(' this gate exists. Fix the test rather than adding it to the baseline.') + console.error('') +} + +function printStaleFailure(stale) { + for (const entry of stale) { + console.error(`::error::Stale tests-typecheck baseline entry (prune it): ${entry}`) + } + console.error('') + console.error('╭────────────────────────────────────────────────────────────────────────────╮') + console.error( + '│ ⚠️ tests-typecheck baseline is out of date — nice work fixing a test! │' + ) + console.error('╰────────────────────────────────────────────────────────────────────────────╯') + console.error('') + console.error(` ${stale.length} baseline entr(y/ies) now typecheck clean.`) + console.error(' The baseline may only shrink, so these must be removed to keep them checked:') + console.error('') + for (const entry of stale) { + console.error(` • ${entry}`) + } + console.error('') + console.error( + ` ✅ Fix it (one command): pnpm --filter orca-mobile check:tests-typecheck --prune` + ) + console.error('') +} + +function printCensusFailure(missing, staleAllowance, nocheck = []) { + for (const entry of missing) { + console.error(`::error::Test file is not in the typecheck program: ${entry}`) + } + for (const entry of staleAllowance) { + console.error(`::error::Stale TESTS_OUTSIDE_PROGRAM entry: ${entry}`) + } + for (const entry of nocheck) { + console.error(`::error::Test file opts out of checking with @ts-nocheck: ${entry}`) + } + console.error('') + console.error('╭────────────────────────────────────────────────────────────────────────────╮') + console.error('│ ❌ mobile tests typecheck census failed — a test file is unchecked. │') + console.error('╰────────────────────────────────────────────────────────────────────────────╯') + console.error('') + if (missing.length > 0) { + console.error(` ${missing.length} test file(s) on disk are outside \`tsc -p ${PROJECT}\`:`) + console.error('') + for (const entry of missing) { + console.error(` • ${entry}`) + } + console.error('') + console.error(' Usual causes: an added `exclude` entry, or a `Foo.test.tsx` shadowed by a') + console.error(' `Foo.test.ts` beside it — a wildcard `include` keeps only the higher-priority') + console.error(' extension, so the .tsx silently leaves the program. Rename one, or exclude it') + console.error(' on purpose by adding it to TESTS_OUTSIDE_PROGRAM with its reason.') + console.error('') + } + if (staleAllowance.length > 0) { + console.error(` ${staleAllowance.length} TESTS_OUTSIDE_PROGRAM entr(y/ies) no longer apply`) + console.error(' (the file is gone, or it is in the program now). Remove them:') + console.error('') + for (const entry of staleAllowance) { + console.error(` • ${entry}`) + } + console.error('') + } + if (nocheck.length > 0) { + console.error(` ${nocheck.length} test file(s) carry @ts-nocheck, which makes tsc exit 0 on`) + console.error(' them. That would let a baselined file be pruned and never checked again:') + console.error('') + for (const entry of nocheck) { + console.error(` • ${entry}`) + } + console.error('') + } +} + +export function main(root = process.cwd()) { + const baselineFile = path.join(root, BASELINE_PATH) + if (!fs.existsSync(baselineFile)) { + console.error( + `::error::Missing mobile/${BASELINE_PATH}. Generate it with: node scripts/check-tests-typecheck-ratchet.mjs --init` + ) + return 1 + } + const pass = collectTypecheckPass(root) + const census = diffCensus(collectTestFilesOnDisk(root), pass.programTestFiles) + const nocheck = findTsNocheckFiles(root, pass.programTestFiles) + if (census.missing.length > 0 || census.staleAllowance.length > 0 || nocheck.length > 0) { + printCensusFailure(census.missing, census.staleAllowance, nocheck) + return 1 + } + + const baseline = parseBaseline(fs.readFileSync(baselineFile, 'utf8')) + const current = pass.failing + const { added, stale } = diffBaseline(current, baseline) + + if (added.length > 0) { + printAddedFailure(added) + if (stale.length > 0) { + printStaleFailure(stale) + } + return 1 + } + if (stale.length > 0) { + printStaleFailure(stale) + return 1 + } + console.log( + `mobile tests typecheck ratchet OK — ${pass.programTestFiles.length} test file(s) in the program (${TESTS_OUTSIDE_PROGRAM.size} excluded on purpose, none @ts-nocheck), ${current.length} grandfathered file(s), every other test file checks.` + ) + return 0 +} + +function writeBaseline(root, entries) { + const header = [ + '# Test files that do NOT yet typecheck under mobile/tsconfig.test.json.', + '# This is a RATCHET: the list may only SHRINK. Do NOT add entries to get CI green —', + '# an unchecked test is one whose type-level pins prove nothing.', + '# Regenerate/prune: node scripts/check-tests-typecheck-ratchet.mjs --prune', + '' + ].join('\n') + fs.writeFileSync(path.join(root, BASELINE_PATH), `${header}${entries.join('\n')}\n`) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const root = process.cwd() + const arg = process.argv[2] + if (arg === '--init') { + const entries = collectCurrentFailingFiles(root) + writeBaseline(root, entries) + console.log(`Wrote mobile/${BASELINE_PATH} with ${entries.length} entries.`) + process.exit(0) + } + if (arg === '--prune') { + const current = new Set(collectCurrentFailingFiles(root)) + const baseline = parseBaseline(fs.readFileSync(path.join(root, BASELINE_PATH), 'utf8')) + const kept = [...baseline].filter((entry) => current.has(entry)).sort() + const newlyAdded = [...current].filter((entry) => !baseline.has(entry)) + writeBaseline(root, kept) + console.log( + `Pruned baseline to ${kept.length} entries (removed ${baseline.size - kept.length}).` + ) + if (newlyAdded.length > 0) { + console.error( + `::error::--prune does not add entries; ${newlyAdded.length} test file(s) newly fail — fix those.` + ) + process.exit(1) + } + process.exit(0) + } + process.exit(main(root)) +} diff --git a/mobile/scripts/check-tests-typecheck-ratchet.test.ts b/mobile/scripts/check-tests-typecheck-ratchet.test.ts new file mode 100644 index 00000000000..3876894b350 --- /dev/null +++ b/mobile/scripts/check-tests-typecheck-ratchet.test.ts @@ -0,0 +1,174 @@ +import fs from 'node:fs' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +// Only the pure functions are under test; the tsc spawn is proven end to end by CI. +import { + diffBaseline, + diffCensus, + hasTsNocheckDirective, + parseBaseline, + parseFailingFiles, + parseProgramTestFiles, + TESTS_OUTSIDE_PROGRAM +} from './check-tests-typecheck-ratchet.mjs' + +describe('the failing-file parser', () => { + it('names each file once however many errors it carries', () => { + const output = [ + 'src/a.test.ts(12,5): error TS2345: Argument of type x.', + 'src/a.test.ts(19,1): error TS2322: Type y.', + 'src/b.test.tsx(3,3): error TS18047: z is possibly null.' + ].join('\n') + expect(parseFailingFiles(output)).toEqual(['src/a.test.ts', 'src/b.test.tsx']) + }) + + // tsc indents the "Overload 1 of 2, ..." detail under its error; counting those as files would + // put unparseable entries in the baseline and make the gate unprunable. + it('ignores the indented detail lines tsc prints under an error', () => { + const output = [ + 'src/a.test.ts(12,5): error TS2769: No overload matches this call.', + " Overload 1 of 2, '(callback: () => Promise): Promise', gave the following error.", + ' Type VitestUtils is missing the following properties.' + ].join('\n') + expect(parseFailingFiles(output)).toEqual(['src/a.test.ts']) + }) + + // tsc prints the host separator. Left as-is, a Windows run would read every baseline entry as + // both stale and added, and the gate would be unpassable rather than wrong in one direction. + it("normalises the Windows separators tsc prints to the baseline's POSIX ones", () => { + const output = [ + 'src\\session\\a.test.ts(12,5): error TS2345: Argument of type x.', + 'scripts\\b.test.tsx(3,3): error TS18047: z is possibly null.' + ].join('\n') + expect(parseFailingFiles(output)).toEqual(['scripts/b.test.tsx', 'src/session/a.test.ts']) + }) + + it('answers nothing for a clean run', () => { + expect(parseFailingFiles('')).toEqual([]) + }) +}) + +describe('the baseline diff', () => { + it('drops comments and blanks when reading the baseline', () => { + expect([...parseBaseline('# header\n\nsrc/a.test.ts\n src/b.test.ts \n')]).toEqual([ + 'src/a.test.ts', + 'src/b.test.ts' + ]) + }) + + it('reports a newly failing file as added and a newly clean one as stale', () => { + expect( + diffBaseline(['src/a.test.ts', 'src/c.test.ts'], ['src/a.test.ts', 'src/b.test.ts']) + ).toEqual({ added: ['src/c.test.ts'], stale: ['src/b.test.ts'] }) + }) + + it('reports neither when the set is unchanged, which is the green path', () => { + expect(diffBaseline(['src/a.test.ts'], ['src/a.test.ts'])).toEqual({ added: [], stale: [] }) + }) +}) + +describe('the program census', () => { + const allow = new Map([['src/node-side.test.ts', 'imports the desktop main process']]) + + // The error diff sees a file only once it errors, so without this an excluded test is silent. + it('names a test file that is on disk but outside the program', () => { + expect( + diffCensus(['src/a.test.ts', 'src/b.test.ts'], ['src/a.test.ts'], allow).missing + ).toEqual(['src/b.test.ts']) + }) + + // A wildcard include keeps only the higher-priority extension, so a .tsx beside a .test.ts of the + // same basename leaves the program with no config change at all. This is the case that hid + // MobileHostCard.test.tsx. + it('names a .tsx shadowed by a .test.ts of the same basename', () => { + expect( + diffCensus(['src/Card.test.ts', 'src/Card.test.tsx'], ['src/Card.test.ts'], new Map()).missing + ).toEqual(['src/Card.test.tsx']) + }) + + it('stays quiet for a file excluded on purpose', () => { + expect(diffCensus(['src/node-side.test.ts'], [], allow)).toEqual({ + missing: [], + staleAllowance: [] + }) + }) + + it('reports an allowance whose file is gone, so the list cannot rot', () => { + expect(diffCensus([], [], allow).staleAllowance).toEqual(['src/node-side.test.ts']) + }) + + it('reports an allowance whose file is back in the program', () => { + expect( + diffCensus(['src/node-side.test.ts'], ['src/node-side.test.ts'], allow).staleAllowance + ).toEqual(['src/node-side.test.ts']) + }) + + it('stays quiet when every file on disk is in the program', () => { + expect(diffCensus(['src/a.test.ts'], ['src/a.test.ts'], new Map())).toEqual({ + missing: [], + staleAllowance: [] + }) + }) + + // The allow-list and the tsconfig exclude are two lists of the same four files; drift between + // them would either break the build or re-open the hole silently. + it('matches tsconfig.test.json exclude entry for entry', () => { + const root = path.join(import.meta.dirname, '..') + const config = fs.readFileSync(path.join(root, 'tsconfig.test.json'), 'utf8') + const excluded = [...config.matchAll(/"([^"]+\.test\.tsx?)"/g)].map((match) => match[1]) + expect(excluded.sort()).toEqual([...TESTS_OUTSIDE_PROGRAM.keys()].sort()) + for (const entry of TESTS_OUTSIDE_PROGRAM.keys()) { + expect(fs.existsSync(path.join(root, entry))).toBe(true) + } + }) +}) + +// One `tsc --noEmit --listFiles` pass answers both questions, so both parsers read the same stream. +describe('the single-pass output split', () => { + const output = [ + '/repo/mobile/node_modules/typescript/lib/lib.es2020.d.ts', + '/repo/mobile/src/session/a.test.ts', + '/repo/mobile/src/session/a.ts', + '/repo/mobile/scripts/b.test.tsx', + 'src/session/a.test.ts(12,5): error TS2345: Argument of type x.', + 'scripts/b.test.tsx(3,3): error TS18047: z is possibly null.' + ].join('\n') + + it('takes only the listed test paths as the program', () => { + expect(parseProgramTestFiles(output, '/repo/mobile')).toEqual([ + 'scripts/b.test.tsx', + 'src/session/a.test.ts' + ]) + }) + + it('takes only the diagnostics as the failures, from that same stream', () => { + expect(parseFailingFiles(output)).toEqual(['scripts/b.test.tsx', 'src/session/a.test.ts']) + }) + + it('strips a Windows root the same way it strips the separators', () => { + expect(parseProgramTestFiles('C:\\repo\\mobile\\src\\a.test.ts', 'C:\\repo\\mobile')).toEqual([ + 'src/a.test.ts' + ]) + }) +}) + +// tsc exits 0 on a @ts-nocheck file, so without this a baselined test could be "fixed" with one +// line, pruned off the baseline, and never checked again. +describe('the @ts-nocheck guard', () => { + it('sees the directive in a leading line comment', () => { + expect(hasTsNocheckDirective('// @ts-nocheck\nimport { it } from "vitest"\n')).toBe(true) + }) + + it('sees it in a leading block comment', () => { + expect(hasTsNocheckDirective('/**\n * @ts-nocheck\n */\nexport {}\n')).toBe(true) + }) + + // TypeScript only honours it before the first statement, so neither should this. + it('ignores it once code has started', () => { + expect(hasTsNocheckDirective('import { it } from "vitest"\n// @ts-nocheck\n')).toBe(false) + }) + + it('stays quiet for an ordinary header comment', () => { + expect(hasTsNocheckDirective('// Tests the reply schema.\nexport {}\n')).toBe(false) + }) +}) diff --git a/mobile/src/agent-history/agent-history-reply-schema.test.ts b/mobile/src/agent-history/agent-history-reply-schema.test.ts new file mode 100644 index 00000000000..35eb9999713 --- /dev/null +++ b/mobile/src/agent-history/agent-history-reply-schema.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest' +import { + agentHistoryHostStatusSchema, + agentHistorySessionScanSchema, + resumeMetadataListSchema, + resumeRepoListSchema +} from './agent-history-reply-schema' + +describe('agent history reply schemas', () => { + it('reads an unreadable capability list as a host that does not advertise the vault', () => { + expect(agentHistoryHostStatusSchema.parse({}).capabilities).toBeUndefined() + expect(agentHistoryHostStatusSchema.parse({ capabilities: 'all' }).capabilities).toBeUndefined() + expect(agentHistoryHostStatusSchema.parse({ capabilities: ['x'] }).capabilities).toEqual(['x']) + }) + + it('passes the rest of the status record through for the platform readers', () => { + expect( + agentHistoryHostStatusSchema.parse({ capabilities: [], platform: 'win32' }) + ).toMatchObject({ platform: 'win32' }) + }) + + it('requires both scan containers the ready screen publishes', () => { + expect(agentHistorySessionScanSchema.safeParse({ sessions: [] }).success).toBe(false) + expect(agentHistorySessionScanSchema.safeParse({ issues: [] }).success).toBe(false) + expect(agentHistorySessionScanSchema.safeParse({ sessions: 'none', issues: [] }).success).toBe( + false + ) + expect(agentHistorySessionScanSchema.safeParse({ sessions: [], issues: [] }).success).toBe(true) + }) + + it('keeps a session row whose agent this build has never heard of', () => { + // The agent vocabulary grows with every CLI Orca learns to scan and is echoed back on resume, + // so a newer host's rows must survive rather than being refused or dropped. + const parsed = agentHistorySessionScanSchema.parse({ + sessions: [{ id: 's1', agent: 'some-new-agent' }], + issues: [] + }) + expect(parsed.sessions).toEqual([{ id: 's1', agent: 'some-new-agent' }]) + }) + + it('requires the resume repo list to be an array', () => { + expect(resumeRepoListSchema.safeParse({}).success).toBe(false) + expect(resumeRepoListSchema.safeParse({ repos: 'none' }).success).toBe(false) + expect(resumeRepoListSchema.parse({ repos: [{ id: 'r' }] }).repos).toEqual([{ id: 'r' }]) + }) + + it('accepts any resume metadata object and refuses a payload that is not one', () => { + expect(resumeMetadataListSchema.parse({ folderWorkspaces: [] })).toMatchObject({ + folderWorkspaces: [] + }) + expect(resumeMetadataListSchema.safeParse('none').success).toBe(false) + expect(resumeMetadataListSchema.safeParse(null).success).toBe(false) + }) +}) diff --git a/mobile/src/agent-history/agent-history-reply-schema.ts b/mobile/src/agent-history/agent-history-reply-schema.ts new file mode 100644 index 00000000000..d0feed40ea8 --- /dev/null +++ b/mobile/src/agent-history/agent-history-reply-schema.ts @@ -0,0 +1,66 @@ +import { z } from 'zod' +import { salvagedOptional } from '../../../src/shared/zod-salvage' + +// The agent-history screen's own reads and the workspace metadata its resume sheet loads. +// Checked against src/main/runtime/rpc/methods/status.ts:6, ai-vault.ts:66, repo.ts:29/42, +// folder-workspace.ts:12 and worktree-catalog-methods.ts:12, and the shared records they return: +// AiVaultSession and AiVaultScanIssue in src/shared/ai-vault-types.ts. + +/** + * The capability list the screen gates the whole surface on. + * + * `capabilities` sits behind main's own `?.includes` and stays salvaged: a reply without a readable + * list reads as "this host does not advertise the vault", which is the `unsupported` screen main + * already showed for it. Individual entries are not narrowed — the list is a growing vocabulary and + * every read is an `includes` against one known token. + * + * The status payload itself is otherwise passed through: the panel hands the whole record to + * readMobileRuntimeHostPlatform and readMobileRuntimeTerminalWindowsShell, both total guards over + * `unknown`, so re-declaring what they read here would narrow two members this screen never + * destructures. + */ +export const agentHistoryHostStatusSchema = z.looseObject({ + capabilities: salvagedOptional('capabilities', z.array(z.string())) +}) + +/** + * The session scan. + * + * Both members are required arrays: use-mobile-agent-history-state.ts:133-135 publishes them straight + * into the ready screen state, where the list maps `sessions` and the issue banner counts `issues` + * — a reply missing either left the screen `ready` over an undefined container and crashed on the + * next render, which is the defect this reader exists to name. + * + * The rows stay unknown, and that is deliberate rather than unfinished. A row is an AiVaultSession, + * whose `agent` is a 21-arm vocabulary that grows with every agent CLI Orca learns to scan — and + * which this client echoes straight back to the host when it resumes a session. Declaring it would + * either refuse a newer host's whole reply or silently drop the very sessions that host added, and + * the remote-wire contract is explicit that a member a client sends back passes through as the host + * wrote it rather than through a client-side fallback. + */ +export const agentHistorySessionScanSchema = z.looseObject({ + sessions: z.array(z.unknown()), + issues: z.array(z.unknown()) +}) + +/** + * The repo identities the resume sheet resolves a session's workspace through. + * + * `repos` is required and an array: loadMobileResumeMetadata reads the member off the payload and + * only then falls back to `[]`, so a null result was a property read on null at the return + * statement and a non-array reached every `repos.find` as one. The rows stay unknown for the same + * reason the session rows do — `executionHostId` is a host-id spelling the resolver already + * degrades, and closing it here would refuse a newer host's own catalog. + */ +export const resumeRepoListSchema = z.looseObject({ repos: z.array(z.unknown()) }) + +/** + * The folder workspaces, project groups and worktrees the resume sheet enriches a target with. + * + * All three are skips whose member read stays at the call site behind `readAcceptedResumeList`, + * which answers `undefined` for anything it cannot read and lets each list degrade to empty. Only + * the container is checked here, because that is the one thing main did not check: a bare string + * reply reached `?.[key]` as `undefined` and the sheet resolved every target to `unknown` with no + * sign anything had gone wrong. + */ +export const resumeMetadataListSchema = z.looseObject({}) diff --git a/mobile/src/agent-history/mobile-agent-history-operations.ts b/mobile/src/agent-history/mobile-agent-history-operations.ts index 7e10f050278..a46a0d1d05b 100644 --- a/mobile/src/agent-history/mobile-agent-history-operations.ts +++ b/mobile/src/agent-history/mobile-agent-history-operations.ts @@ -1,5 +1,11 @@ import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' -import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' +import { rpcResultVariant } from '../transport/rpc-operation-result-reader' +import { + agentHistoryHostStatusSchema, + agentHistorySessionScanSchema, + resumeMetadataListSchema, + resumeRepoListSchema +} from './agent-history-reply-schema' // The agent-history screen's own reads: the capability gate and session scan it runs on open, and // the workspace metadata the resume sheet loads once the user asks to resume a session. @@ -16,7 +22,7 @@ export const agentHistoryHostStatusRead = bindDeferredRpcOperation( method: 'status.get', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('host-status') + read: rpcResultVariant('host-status', agentHistoryHostStatusSchema) }) ) @@ -26,7 +32,7 @@ export const agentHistorySessionScan = bindDeferredRpcOperation( method: 'aiVault.listSessions', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('agent-sessions') + read: rpcResultVariant('agent-sessions', agentHistorySessionScanSchema) }) ) @@ -42,7 +48,7 @@ export const resumeRepoListRead = bindDeferredRpcOperation( method: 'repo.list', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('resume-repos') + read: rpcResultVariant('resume-repos', resumeRepoListSchema) }) ) @@ -56,7 +62,7 @@ export const resumeFolderWorkspaceListRead = bindDeferredRpcOperation( method: 'folderWorkspace.list', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('resume-folder-workspaces') + read: rpcResultVariant('resume-folder-workspaces', resumeMetadataListSchema) }) ) @@ -66,7 +72,7 @@ export const resumeProjectGroupListRead = bindDeferredRpcOperation( method: 'projectGroup.list', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('resume-project-groups') + read: rpcResultVariant('resume-project-groups', resumeMetadataListSchema) }) ) @@ -76,6 +82,6 @@ export const resumeWorktreeListRead = bindDeferredRpcOperation( method: 'worktree.ps', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('resume-worktrees') + read: rpcResultVariant('resume-worktrees', resumeMetadataListSchema) }) ) diff --git a/mobile/src/agent-history/use-mobile-agent-history-state.ts b/mobile/src/agent-history/use-mobile-agent-history-state.ts index d79eb89937b..5eea3cfdb62 100644 --- a/mobile/src/agent-history/use-mobile-agent-history-state.ts +++ b/mobile/src/agent-history/use-mobile-agent-history-state.ts @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useHostClient, useForceReconnect } from '../transport/client-context' import type { - AiVaultListResult, AiVaultScanIssue, AiVaultScope, AiVaultSession @@ -28,8 +27,6 @@ export type AgentHistoryScreenState = | { kind: 'error'; message: string } | { kind: 'ready'; sessions: AiVaultSession[]; issues: AiVaultScanIssue[] } -type StatusWithCapabilities = { capabilities?: string[] } - export type MobileAgentHistoryStateParams = { hostId: string worktreeId: string @@ -96,11 +93,10 @@ export function useMobileAgentHistoryState(params: MobileAgentHistoryStateParams if (!isCurrent()) { return } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. const status = interpretOrThrowRefusalMessage( () => agentHistoryHostStatusRead.interpret(statusReply), 'Unable to reach host' - ) as StatusWithCapabilities + ) setHostStatusResult(status) if (!status.capabilities?.includes(MOBILE_AI_VAULT_CAPABILITY)) { setScreenState({ kind: 'unsupported' }) @@ -127,12 +123,17 @@ export function useMobileAgentHistoryState(params: MobileAgentHistoryStateParams if (!isCurrent()) { return } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. const result = interpretOrThrowRefusalMessage( () => agentHistorySessionScan.interpret(reply), 'Unable to load agent sessions' - ) as AiVaultListResult - setScreenState({ kind: 'ready', sessions: result.sessions, issues: result.issues }) + ) + setScreenState({ + kind: 'ready', + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the reader checks both containers; the rows stay the host's own records because `agent` is a vocabulary this client echoes back on resume. aivault-history-screen-listed `normal` records a full row: every member the cards and the resume path read unguarded. + sessions: result.sessions as AiVaultSession[], + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: same reader, same container check; the issue rows are the host's AiVaultScanIssue and are only counted, never read member-wise (MobileAgentSessionHistoryPanel.tsx:335). + issues: result.issues as AiVaultScanIssue[] + }) } catch (err) { if (!isCurrent()) { return diff --git a/mobile/src/components/MobileDictationSetupSheet.tsx b/mobile/src/components/MobileDictationSetupSheet.tsx index f4a81c02b8f..f590b10082f 100644 --- a/mobile/src/components/MobileDictationSetupSheet.tsx +++ b/mobile/src/components/MobileDictationSetupSheet.tsx @@ -25,7 +25,7 @@ type Props = { onReady?: () => void } -function formatSize(bytes: number | null): string { +function formatSize(bytes: number | null | undefined): string { if (!bytes) { return '' } diff --git a/mobile/src/components/MobileHostCard.test.tsx b/mobile/src/components/MobileHostCard-truthful-status.test.tsx similarity index 100% rename from mobile/src/components/MobileHostCard.test.tsx rename to mobile/src/components/MobileHostCard-truthful-status.test.tsx diff --git a/mobile/src/components/MobileRepoIcon.tsx b/mobile/src/components/MobileRepoIcon.tsx index 2ea1f8d573b..b1c352382ee 100644 --- a/mobile/src/components/MobileRepoIcon.tsx +++ b/mobile/src/components/MobileRepoIcon.tsx @@ -23,7 +23,7 @@ import { Wrench } from 'lucide-react-native' import { Image, StyleSheet, Text, View } from 'react-native' -import type { RepoIcon } from '../../../src/shared/repo-icon' +import type { MobileRenderableRepoIcon } from '../host-screen/host-screen-reply-schema' import { colors } from '../theme/mobile-theme' // The lucide names the desktop repo-icon picker offers (src/renderer/src/ @@ -53,7 +53,9 @@ const REPO_LUCIDE_ICONS: Record = { } type Props = { - repoIcon?: RepoIcon | null + // Why the decoded catalog icon rather than the host's `RepoIcon`: this component reads + // `type`/`src`/`label`/`emoji`/`name` and never `source`, and a `RepoIcon` still satisfies it. + repoIcon?: MobileRenderableRepoIcon | null size?: number color?: string } diff --git a/mobile/src/components/VoiceModelList.tsx b/mobile/src/components/VoiceModelList.tsx index 4151f2591ad..a776945a075 100644 --- a/mobile/src/components/VoiceModelList.tsx +++ b/mobile/src/components/VoiceModelList.tsx @@ -17,7 +17,7 @@ type Props = { onDelete: (model: MobileSpeechModel) => void } -function formatSize(bytes: number | null): string { +function formatSize(bytes: number | null | undefined): string { if (!bytes) { return '' } diff --git a/mobile/src/components/WorktreeListRow.tsx b/mobile/src/components/WorktreeListRow.tsx index 1de862e630c..98684eab449 100644 --- a/mobile/src/components/WorktreeListRow.tsx +++ b/mobile/src/components/WorktreeListRow.tsx @@ -10,9 +10,9 @@ import { } from 'lucide-react-native' import { Pressable, StyleSheet, Text, View } from 'react-native' import { parseExecutionHostId, type ExecutionHostId } from '../../../src/shared/execution-host' -import type { RepoIcon } from '../../../src/shared/repo-icon' import type { AgentWorkingMode } from '../../../src/shared/agent-status-types' import type { RuntimeWorktreeAgentRow } from '../../../src/shared/runtime-types' +import type { MobileRenderableRepoIcon } from '../host-screen/host-screen-reply-schema' import { triggerMediumImpact } from '../platform/haptics' import { colors, radii, spacing, typography } from '../theme/mobile-theme' import { AgentSpinner } from './AgentSpinner' @@ -64,7 +64,7 @@ type Props = { isReadOnly: boolean now: number repoColor: string - repoIcon?: RepoIcon | null + repoIcon?: MobileRenderableRepoIcon | null // When the list is already grouped under this repo's section header, the row // omits its own repo icon+name to avoid the redundant "📁 orca" on every row. hideRepo?: boolean diff --git a/mobile/src/components/new-workspace-reply-schema.test.ts b/mobile/src/components/new-workspace-reply-schema.test.ts index 2d0862f3441..ebacbdfc27e 100644 --- a/mobile/src/components/new-workspace-reply-schema.test.ts +++ b/mobile/src/components/new-workspace-reply-schema.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest' import type { z } from 'zod' import { newWorkspaceRepoHooksSchema, - newWorkspaceUiTrustSchema + newWorkspaceUiTrustSchema, + SETUP_RUN_POLICIES } from './new-workspace-reply-schema' function reads(schema: z.ZodType, value: unknown): T { @@ -44,8 +45,9 @@ describe('the drawer requires only what it reads unguarded', () => { }) describe('the setup run policy is a closed enum with the call site defaulting it', () => { + // The arm list is pinned to SetupRunPolicy in the schema module, where tsc looks. it('keeps each arm the drawer compares against', () => { - for (const policy of ['ask', 'run-by-default', 'skip-by-default'] as const) { + for (const policy of SETUP_RUN_POLICIES) { expect( reads(newWorkspaceRepoHooksSchema, { source: null, setupRunPolicy: policy }).setupRunPolicy ).toBe(policy) diff --git a/mobile/src/components/new-workspace-reply-schema.ts b/mobile/src/components/new-workspace-reply-schema.ts index 22e8b3f4557..f5c3b04103b 100644 --- a/mobile/src/components/new-workspace-reply-schema.ts +++ b/mobile/src/components/new-workspace-reply-schema.ts @@ -1,10 +1,18 @@ import { z } from 'zod' -import { salvagedOptional, salvagingRecord } from '../../../src/shared/zod-salvage' +import type { SetupRunPolicy } from '../../../src/shared/orca-yaml-hook-types' +import { hostUnionArms, salvagedOptional, salvagingRecord } from '../../../src/shared/zod-salvage' // The New Workspace drawer's own two reads. Checked against `repo.hooks` // (src/main/runtime/rpc/methods/repo.ts:190 → runtime.getRepoHooks) and `ui.get` // (src/main/runtime/rpc/methods/client-ui.ts:60), which answers `{ ui }` and nothing else. +// Pinned to the host's own union through hostUnionArms: an arm added or dropped host-side fails tsc. +export const SETUP_RUN_POLICIES = hostUnionArms({ + ask: true, + 'run-by-default': true, + 'skip-by-default': true +}) + /** * The repo's setup hook, as the drawer decorates its advanced section with it. * @@ -19,7 +27,8 @@ import { salvagedOptional, salvagingRecord } from '../../../src/shared/zod-salva * site's `?? 'run-by-default'` still doing the defaulting: the only two comparisons against it are * `!== 'skip-by-default'` and `=== 'ask'`, so an arm this build does not know behaves exactly as * main's unrecognised string did. The arms are the host's `SetupRunPolicy` - * (src/shared/orca-yaml-hook-types.ts:1), which is what `getEffectiveSetupRunPolicy` answers. `setupTrust` is nullable as well as optional because the + * (src/shared/orca-yaml-hook-types.ts:1), which is what `getEffectiveSetupRunPolicy` answers, and + * they are pinned to it above. `setupTrust` is nullable as well as optional because the * `components-setup-ask` fixture sends an explicit `null` — salvaging that as a drop would move a * `normal` golden for a reply the host really sends. */ @@ -36,10 +45,7 @@ export const newWorkspaceRepoHooksSchema = z.looseObject({ .nullable() ), source: z.string().nullable(), - setupRunPolicy: salvagedOptional( - 'setupRunPolicy', - z.enum(['ask', 'run-by-default', 'skip-by-default']) - ), + setupRunPolicy: salvagedOptional('setupRunPolicy', z.enum(SETUP_RUN_POLICIES)), setupTrust: salvagedOptional( 'setupTrust', z.looseObject({ contentHash: z.string(), scriptContent: z.string() }).nullable() diff --git a/mobile/src/components/use-new-worktree-drawer-navigation.test.ts b/mobile/src/components/use-new-worktree-drawer-navigation.test.ts index 347e309c5e0..83c4a03f326 100644 --- a/mobile/src/components/use-new-worktree-drawer-navigation.test.ts +++ b/mobile/src/components/use-new-worktree-drawer-navigation.test.ts @@ -45,7 +45,9 @@ describe('useNewWorktreeDrawerNavigation', () => { expect(nav.current.drawerView).toBe('transition') expect(nav.current.formSheetVisible).toBe(true) - act(() => vi.advanceTimersByTime(500)) + act(() => { + vi.advanceTimersByTime(500) + }) expect(nav.current.drawerView).toBe('agent') expect(nav.current.formSheetVisible).toBe(false) }) @@ -54,7 +56,9 @@ describe('useNewWorktreeDrawerNavigation', () => { const nav = renderNavigation(true) act(() => nav.current.openSourceDrawer()) act(() => nav.current.transitionDrawer('form')) - act(() => vi.advanceTimersByTime(500)) + act(() => { + vi.advanceTimersByTime(500) + }) expect(nav.current.drawerView).toBe('form') expect(nav.current.formSheetVisible).toBe(true) diff --git a/mobile/src/diagnostics/mobile-web-bundle-probe-row.tsx b/mobile/src/diagnostics/mobile-web-bundle-probe-row.tsx new file mode 100644 index 00000000000..06a1db2efcb --- /dev/null +++ b/mobile/src/diagnostics/mobile-web-bundle-probe-row.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from 'react' +import { View, Text, Pressable, ActivityIndicator } from 'react-native' +import { Package } from 'lucide-react-native' +import { loadHosts } from '../transport/host-store' +import { colors } from '../theme/mobile-theme' +import { troubleshootScreenStyles as styles } from './troubleshoot-screen-styles' +import { + useMobileWebBundleProbe, + type MobileWebBundleProbeState +} from './use-mobile-web-bundle-probe' +import type { HostProfile } from '../transport/types' + +/** Dialling the host is part of the tap, so the label says which half is still running. */ +function buttonLabel(state: MobileWebBundleProbeState, awaitingHost: boolean): string { + if (state.status !== 'running') { + return 'Fetch mobile web bundle' + } + return awaitingHost ? 'Connecting…' : 'Fetching bundle…' +} + +/** One `label — detail` line in the same row shape the diagnostic checks use. */ +function ProbeLine({ label, detail, failed }: { label: string; detail: string; failed?: boolean }) { + return ( + + {label} + {detail} + + ) +} + +function ProbeResult({ state, hostName }: { state: MobileWebBundleProbeState; hostName: string }) { + if (state.status === 'idle' || state.status === 'running') { + return null + } + if (state.status === 'failed') { + return ( + + + + ) + } + return ( + + + + + + + + + + + + ) +} + +/** + * Development-only: fetches the whole mobile web bundle from a paired desktop and reports what came + * back. Phase A ships no production path that renders a bundle, and this row is the only thing that + * exercises the operations end to end on a device. + * + * `app/troubleshoot.tsx` mounts it behind `__DEV__`, so a shipped build never runs the host lookup. + * + * The screen carries no host parameter and troubleshoots every paired host, one reachability check + * each, so there is no host it is "on". The probe takes the first paired host and names it in the + * result rather than implying it speaks for all of them. + */ +export function MobileWebBundleProbeRow() { + const [hosts, setHosts] = useState([]) + useEffect(() => { + let stale = false + void loadHosts().then((loaded) => { + if (!stale) { + setHosts(loaded) + } + }) + return () => { + stale = true + } + }, []) + const host = hosts[0] ?? null + const { state, run, awaitingHost } = useMobileWebBundleProbe(host?.id ?? null) + + return ( + + [ + styles.diagnosticButton, + pressed && styles.diagnosticButtonPressed, + state.status === 'running' && styles.diagnosticButtonDisabled + ]} + testID="mobile-web-bundle-probe" + onPress={run} + disabled={state.status === 'running'} + > + {state.status === 'running' ? ( + + ) : ( + + )} + {buttonLabel(state, awaitingHost)} + + + + ) +} diff --git a/mobile/src/diagnostics/troubleshoot-view.tsx b/mobile/src/diagnostics/troubleshoot-view.tsx index 0fd86605f13..8655ddbce4c 100644 --- a/mobile/src/diagnostics/troubleshoot-view.tsx +++ b/mobile/src/diagnostics/troubleshoot-view.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react' +import { useCallback, useState, type ReactNode } from 'react' import { View, Text, Pressable, ScrollView, ActivityIndicator } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { @@ -39,7 +39,8 @@ export function TroubleshootView({ checks, runDiagnostics, onBack, - onConnectionLog + onConnectionLog, + developerRow }: { rootRef?: (node: View | null) => void diagnosticStatus: DiagnosticStatus @@ -47,6 +48,8 @@ export function TroubleshootView({ runDiagnostics: () => void onBack: () => void onConnectionLog: () => void + /** Slot the route fills only under `__DEV__`; null in every shipped build. */ + developerRow?: ReactNode }) { const insets = useSafeAreaInsets() const [expandedId, setExpandedId] = useState(null) @@ -108,6 +111,8 @@ export function TroubleshootView({ View network diagnostics + {developerRow} + {checks.length > 0 && ( {checks.map((check, i) => ( diff --git a/mobile/src/diagnostics/use-mobile-web-bundle-probe.test.tsx b/mobile/src/diagnostics/use-mobile-web-bundle-probe.test.tsx new file mode 100644 index 00000000000..e339d6125de --- /dev/null +++ b/mobile/src/diagnostics/use-mobile-web-bundle-probe.test.tsx @@ -0,0 +1,305 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { RpcClient } from '../transport/rpc-client' +import type { MobileWebBundleFetchResult } from '../transport/mobile-web-bundle-fetch' + +const push = vi.hoisted(() => ({ attach: vi.fn(), detach: vi.fn() })) +vi.mock('../notifications/push-registration', () => ({ attachPushRegistration: push.attach })) + +const connectMock = vi.hoisted(() => vi.fn()) +const loadHostsMock = vi.hoisted(() => vi.fn()) +const fetchMock = vi.hoisted(() => vi.fn()) + +vi.mock('../transport/rpc-client', () => ({ + connect: (...args: unknown[]) => connectMock(...args) +})) +vi.mock('../transport/host-logical-client', () => ({ + openHostLogicalClient: (...args: unknown[]) => connectMock(...args) +})) +vi.mock('../transport/host-store', () => ({ loadHosts: () => loadHostsMock() })) +vi.mock('../transport/connection-revival-triggers', () => ({ + subscribeConnectionRevivalTriggers: () => () => {} +})) +vi.mock('../transport/mobile-web-bundle-fetch', () => ({ + fetchMobileWebBundle: (...args: unknown[]) => fetchMock(...args) +})) + +import { RpcClientProvider } from '../transport/client-context' +import { + useMobileWebBundleProbe, + type MobileWebBundleProbeState +} from './use-mobile-web-bundle-probe' + +const HOST = { + id: 'host-1', + name: 'Host 1', + endpoint: 'ws://127.0.0.1:1', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 0 +} + +function fakeClient(): RpcClient { + return { + sendRequest: vi.fn(), + subscribe: vi.fn(() => () => {}), + updateTerminalSubscriptionViewport: vi.fn(), + getState: () => 'connected', + getReconnectAttempt: () => 0, + getLastConnectedAt: () => null, + onStateChange: () => () => {}, + notifyForeground: vi.fn(), + close: vi.fn() + } +} + +type ProbeHarness = { + readonly state: MobileWebBundleProbeState + readonly awaitingHost: boolean + run: () => Promise + unmount: () => Promise +} + +async function renderProbe(hostId: string | null): Promise { + let latest: ReturnType | null = null + // A box, not a `let`: assigning inside the callback leaves a `let` narrowed to `null`. + const rendered: { tree: ReactTestRenderer | null } = { tree: null } + + function Probe(): null { + latest = useMobileWebBundleProbe(hostId) + return null + } + + await act(async () => { + rendered.tree = create(createElement(RpcClientProvider, null, createElement(Probe))) + }) + const read = () => { + if (!latest) { + throw new Error('probe did not render') + } + return latest + } + return { + get state() { + return read().state + }, + get awaitingHost() { + return read().awaitingHost + }, + run: async () => { + await act(async () => { + read().run() + }) + }, + unmount: async () => { + await act(async () => { + rendered.tree?.unmount() + }) + } + } +} + +function fetchedBundle(): MobileWebBundleFetchResult { + return { + manifest: { + schemaVersion: 1, + buildId: 'a'.repeat(64), + entrypoint: 'index.html', + totalBytes: 3, + assets: [ + { path: 'index.html', sha256: 'b'.repeat(64), byteLength: 3, contentType: 'text/html' } + ] + }, + assets: new Map([['index.html', new Uint8Array([1, 2, 3])]]), + totalBytes: 3, + elapsedMs: 12 + } +} + +type Deferred = { + readonly promise: Promise + resolve: (value: T) => void + reject: (error: unknown) => void +} + +function deferred(): Deferred { + const box: { resolve: (value: T) => void; reject: (error: unknown) => void } = { + resolve: () => {}, + reject: () => {} + } + const promise = new Promise((resolve, reject) => { + box.resolve = resolve + box.reject = reject + }) + return { promise, resolve: box.resolve, reject: box.reject } +} + +async function settle(): Promise { + await act(async () => { + await Promise.resolve() + }) +} + +beforeEach(() => { + push.attach.mockReset().mockReturnValue(push.detach) + push.detach.mockReset() + connectMock.mockReset().mockReturnValue(fakeClient()) + loadHostsMock.mockReset().mockResolvedValue([HOST]) + fetchMock.mockReset() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +describe('useMobileWebBundleProbe', () => { + it('dials no host until the row is tapped', async () => { + const probe = await renderProbe(HOST.id) + + expect(connectMock).not.toHaveBeenCalled() + expect(fetchMock).not.toHaveBeenCalled() + expect(probe.state).toEqual({ status: 'idle' }) + + fetchMock.mockResolvedValue(fetchedBundle()) + await probe.run() + + expect(connectMock).toHaveBeenCalledTimes(1) + expect(fetchMock).toHaveBeenCalledTimes(1) + expect(probe.state).toEqual({ + status: 'done', + buildId: 'a'.repeat(64), + assetCount: 1, + totalBytes: 3, + elapsedMs: 12 + }) + expect(probe.awaitingHost).toBe(false) + }) + + it('reports the host code when the desktop refused', async () => { + fetchMock.mockRejectedValue(new Error('invalid_argument: mobile_web_bundle_unavailable')) + const probe = await renderProbe(HOST.id) + + await probe.run() + + expect(probe.state).toEqual({ status: 'failed', detail: 'mobile_web_bundle_unavailable' }) + }) + + it('reports a schema refusal, which carries no code, as its message', async () => { + fetchMock.mockRejectedValue( + new Error('invalid_argument: Invalid input: expected string, received number') + ) + const probe = await renderProbe(HOST.id) + + await probe.run() + + expect(probe.state).toEqual({ + status: 'failed', + detail: 'invalid_argument: Invalid input: expected string, received number' + }) + }) + + it('fails without dialling when no host is paired', async () => { + const probe = await renderProbe(null) + + await probe.run() + + expect(connectMock).not.toHaveBeenCalled() + expect(fetchMock).not.toHaveBeenCalled() + expect(probe.state).toEqual({ status: 'failed', detail: 'no paired host to fetch from' }) + }) + + it('gives up on a host whose client never arrives instead of waiting forever', async () => { + vi.useFakeTimers() + // The host is not in the store, so no client is ever acquired for it and `awaitingHost` would + // otherwise stay true with the row's button disabled for the life of the screen. + loadHostsMock.mockResolvedValue([]) + const probe = await renderProbe(HOST.id) + + await probe.run() + expect(probe.state).toEqual({ status: 'running' }) + expect(probe.awaitingHost).toBe(true) + + await act(async () => { + await vi.advanceTimersByTimeAsync(9_999) + }) + expect(probe.state).toEqual({ status: 'running' }) + + await act(async () => { + await vi.advanceTimersByTimeAsync(1) + }) + expect(probe.state).toEqual({ status: 'failed', detail: 'no client for the host within 10s' }) + // The row re-enables its button off `running`, and nothing is left dialling the host. + expect(probe.awaitingHost).toBe(false) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('does not clear the deadline for a host that did arrive', async () => { + vi.useFakeTimers() + fetchMock.mockReturnValue(new Promise(() => {})) + const probe = await renderProbe(HOST.id) + + await probe.run() + expect(fetchMock).toHaveBeenCalledTimes(1) + + await act(async () => { + await vi.advanceTimersByTimeAsync(60_000) + }) + + // A slow fetch is not a host that never opened: the deadline covers acquiring the client only. + expect(probe.state).toEqual({ status: 'running' }) + }) + + it('ignores a result from a run the screen already moved on from', async () => { + const first = deferred() + const second = deferred() + fetchMock.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise) + const probe = await renderProbe(HOST.id) + + await probe.run() + await probe.run() + first.resolve({ ...fetchedBundle(), elapsedMs: 999 }) + await settle() + + expect(probe.state).toEqual({ status: 'running' }) + + second.resolve(fetchedBundle()) + await settle() + + expect(probe.state).toMatchObject({ status: 'done', elapsedMs: 12 }) + }) + + it('ignores a failure from a run the screen already moved on from', async () => { + const first = deferred() + const second = deferred() + fetchMock.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise) + const probe = await renderProbe(HOST.id) + + await probe.run() + await probe.run() + first.reject(new Error('invalid_argument: mobile_web_bundle_unavailable')) + await settle() + + expect(probe.state).toEqual({ status: 'running' }) + + second.resolve(fetchedBundle()) + await settle() + + expect(probe.state).toMatchObject({ status: 'done', elapsedMs: 12 }) + }) + + it('aborts the run it started when the screen goes away', async () => { + const captured: { signal: AbortSignal | null } = { signal: null } + fetchMock.mockImplementation((args: { signal?: AbortSignal }) => { + captured.signal = args.signal ?? null + return new Promise(() => {}) + }) + const probe = await renderProbe(HOST.id) + await probe.run() + + expect(captured.signal?.aborted).toBe(false) + await probe.unmount() + + expect(captured.signal?.aborted).toBe(true) + }) +}) diff --git a/mobile/src/diagnostics/use-mobile-web-bundle-probe.ts b/mobile/src/diagnostics/use-mobile-web-bundle-probe.ts new file mode 100644 index 00000000000..aeebb4573a5 --- /dev/null +++ b/mobile/src/diagnostics/use-mobile-web-bundle-probe.ts @@ -0,0 +1,121 @@ +import { useCallback, useEffect, useState } from 'react' +import { useHostClient } from '../transport/client-context' +import { fetchMobileWebBundle } from '../transport/mobile-web-bundle-fetch' +import { readMobileWebBundleErrorCode } from '../transport/mobile-web-bundle-operations' +import { startDiagnosticFetchTimeout } from './diagnostic-fetch-timeout' + +/** + * How long a tap waits for the host's client object before it gives up. + * + * Generous, because acquiring one can queue behind another screen's, but bounded, because none of + * that is a network round trip: the connect and request timeouts live below this, inside the fetch, + * and only apply once a client exists. Without a bound here a host that never opens leaves the row + * reading `Connecting…` with its button disabled for the life of the screen. + */ +const HOST_CLIENT_DIAL_DEADLINE_MS = 10_000 + +export type MobileWebBundleProbeState = + | { status: 'idle' } + | { status: 'running' } + | { + status: 'done' + buildId: string + assetCount: number + totalBytes: number + elapsedMs: number + } + | { status: 'failed'; detail: string } + +/** The host's own code when it refused, its message otherwise. A client-side integrity failure has + * no code, and so does a schema refusal the dispatcher raised before the handler ran. */ +function describeFailure(error: unknown): string { + const code = readMobileWebBundleErrorCode(error) + if (code !== null) { + return code + } + return error instanceof Error ? error.message : String(error) +} + +/** + * Drives one bundle fetch from the troubleshooting screen. Dev-only: nothing in a shipped build + * mounts this, and nothing here caches or renders what it downloads. + * + * The host is dialled on the first tap, not on mount: acquiring a client is what opens a connection, + * and opening Troubleshoot opened none before this row existed. Each request owns its + * `AbortController` so a re-run, an unmount, or StrictMode's second mount abandons the previous + * fetch instead of racing it — and, since the fetch checks that signal before every chunk, stops its + * reads rather than letting them hold the host's read slots. + */ +export function useMobileWebBundleProbe(hostId: string | null): { + state: MobileWebBundleProbeState + run: () => void + awaitingHost: boolean +} { + const [state, setState] = useState({ status: 'idle' }) + const [request, setRequest] = useState<{ id: number } | null>(null) + const { client } = useHostClient(request !== null && hostId !== null ? hostId : undefined) + + useEffect(() => { + if (request === null || client === null) { + return + } + let abandoned = false + const controller = new AbortController() + fetchMobileWebBundle({ client, signal: controller.signal }).then( + (fetched) => { + if (abandoned) { + return + } + setState({ + status: 'done', + buildId: fetched.manifest.buildId, + assetCount: fetched.assets.size, + totalBytes: fetched.totalBytes, + elapsedMs: fetched.elapsedMs + }) + }, + (error: unknown) => { + if (abandoned) { + return + } + setState({ status: 'failed', detail: describeFailure(error) }) + } + ) + return () => { + abandoned = true + controller.abort() + } + }, [client, request]) + + useEffect(() => { + if (request === null || client !== null) { + return + } + const deadline = startDiagnosticFetchTimeout(HOST_CLIENT_DIAL_DEADLINE_MS) + const giveUp = () => { + setState({ + status: 'failed', + detail: `no client for the host within ${HOST_CLIENT_DIAL_DEADLINE_MS / 1000}s` + }) + // Drops the acquisition too, so a host that never opens stops being dialled. + setRequest(null) + } + deadline.signal.addEventListener('abort', giveUp) + return () => { + // Removed first: `dispose` aborts a signal it has not already aborted. + deadline.signal.removeEventListener('abort', giveUp) + deadline.dispose() + } + }, [client, request]) + + const run = useCallback(() => { + if (hostId === null) { + setState({ status: 'failed', detail: 'no paired host to fetch from' }) + return + } + setState({ status: 'running' }) + setRequest((previous) => ({ id: (previous?.id ?? 0) + 1 })) + }, [hostId]) + + return { state, run, awaitingHost: request !== null && client === null } +} diff --git a/mobile/src/dictation/dictation-reply-schema.test.ts b/mobile/src/dictation/dictation-reply-schema.test.ts new file mode 100644 index 00000000000..aa241d885a6 --- /dev/null +++ b/mobile/src/dictation/dictation-reply-schema.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'vitest' +import { + dictationSetupSchema, + SPEECH_MODEL_PROVIDERS, + SPEECH_MODEL_STATUSES +} from './dictation-reply-schema' + +const setup = (overrides: Record = {}) => ({ + enabled: true, + selectedModelId: 'm1', + dictationMode: 'toggle', + models: [], + ...overrides +}) + +describe('dictation setup reply schema', () => { + it('requires the model list the sheet maps unguarded', () => { + const { models: _models, ...withoutModels } = setup() + expect(dictationSetupSchema.safeParse(withoutModels).success).toBe(false) + expect(dictationSetupSchema.safeParse(setup({ models: 'none' })).success).toBe(false) + }) + + it('forwards a dictation mode this build has never heard of instead of substituting one', () => { + expect(dictationSetupSchema.parse(setup({ dictationMode: 'push-to-talk' })).dictationMode).toBe( + 'push-to-talk' + ) + expect(dictationSetupSchema.parse(setup({ dictationMode: 'hold' })).dictationMode).toBe('hold') + }) + + it('keeps a sheet whose mode is absent or unreadable, which main rendered', () => { + const { dictationMode: _mode, ...withoutMode } = setup() + expect(dictationSetupSchema.parse(withoutMode).dictationMode).toBeUndefined() + expect(dictationSetupSchema.parse(setup({ dictationMode: 3 })).dictationMode).toBeUndefined() + }) + + it('salvages enabled and selectedModelId onto the off-and-unselected sheet main drew', () => { + const parsed = dictationSetupSchema.parse(setup({ enabled: 'yes', selectedModelId: 4 })) + expect(parsed.enabled).toBeUndefined() + expect(parsed.selectedModelId).toBeUndefined() + }) + + it('drops a model row the sheet could not address or send back', () => { + const parsed = dictationSetupSchema.parse( + setup({ models: [{ id: 'a', label: 'A' }, { label: 'B' }, { id: 'c' }] }) + ) + expect(parsed.models.map((model) => model.id)).toEqual(['a', 'c']) + }) + + it('degrades an unknown provider or status arm to absent and keeps the row', () => { + const [model] = dictationSetupSchema.parse( + setup({ models: [{ id: 'a', provider: 'anthropic', status: 'verifying' }] }) + ).models + expect(model?.id).toBe('a') + expect(model?.provider).toBeUndefined() + expect(model?.status).toBeUndefined() + }) + + it('keeps the known provider and status arms', () => { + const [model] = dictationSetupSchema.parse( + setup({ models: [{ id: 'a', provider: 'openai', status: 'extracting' }] }) + ).models + expect(model?.provider).toBe('openai') + expect(model?.status).toBe('extracting') + }) + + it('salvages a row member behind main own guard and keeps the truthy recommended flag', () => { + const [model] = dictationSetupSchema.parse( + setup({ models: [{ id: 'a', sizeBytes: 'big', progress: 'half', recommended: 1 }] }) + ).models + expect(model?.sizeBytes).toBeUndefined() + expect(model?.progress).toBeUndefined() + expect(model?.recommended).toBe(1) + }) + + it('keeps an explicit null size and progress, which the host sends for an API model', () => { + const [model] = dictationSetupSchema.parse( + setup({ models: [{ id: 'a', sizeBytes: null, progress: null }] }) + ).models + expect(model?.sizeBytes).toBeNull() + expect(model?.progress).toBeNull() + }) + + it('passes a newer host member through', () => { + expect(dictationSetupSchema.parse(setup({ hotword: 'orca' }))).toMatchObject({ + hotword: 'orca' + }) + }) +}) + +describe('provider and status are closed over the host union', () => { + // The arm lists are pinned to RuntimeSpeechModelSummary in the schema module, where tsc looks; + // this loop proves every pinned arm survives the parse, not just the two the tests above pick. + it('keeps every arm the host declares, so nothing it sends today degrades', () => { + for (const provider of SPEECH_MODEL_PROVIDERS) { + for (const status of SPEECH_MODEL_STATUSES) { + const [model] = dictationSetupSchema.parse( + setup({ models: [{ id: 'a', provider, status }] }) + ).models + expect(model).toMatchObject({ provider, status }) + } + } + }) +}) diff --git a/mobile/src/dictation/dictation-reply-schema.ts b/mobile/src/dictation/dictation-reply-schema.ts new file mode 100644 index 00000000000..bf35f3fcf7b --- /dev/null +++ b/mobile/src/dictation/dictation-reply-schema.ts @@ -0,0 +1,93 @@ +import { z } from 'zod' +import type { RuntimeSpeechModelSummary } from '../../../src/shared/runtime-worktree-contracts' +import { hostUnionArms, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage' + +// The dictation setup sheet's reads and writes, and the three sends one dictation session makes. +// Checked against src/main/runtime/rpc/methods/speech.ts:12-59 and the shared results the speech +// catalog and dictation controller return: RuntimeSpeechSetupState and RuntimeSpeechModelSummary in +// src/shared/runtime-worktree-contracts.ts:81-96. + +// Pinned to the host's own union through hostUnionArms: an arm added or dropped host-side fails tsc. +export const SPEECH_MODEL_PROVIDERS = hostUnionArms({ + local: true, + openai: true +}) +export const SPEECH_MODEL_STATUSES = hostUnionArms({ + ready: true, + 'not-downloaded': true, + downloading: true, + extracting: true, + error: true +}) + +/** + * One model row in the setup sheet. + * + * `id` is required: it is the React key the list renders by, the value `isSelected` compares + * against, and — the reason it cannot be salvaged — the `modelId` the download, delete and select + * sends put back on the wire. A row without one drops rather than failing the whole sheet, which is + * the same rule the terminal inventory uses for a row it cannot address. + * + * `provider` and `status` are closed arm sets on the wire, so an arm this build has not heard of + * degrades to absent rather than refusing the reply or dropping the row. Absent is the right + * degrade here and not a coincidence: every read of either is an equality test against a known arm + * (`provider === 'openai'`, `status === 'ready'`, isModelInFlight), so an unknown arm already fell + * through to the same branch on main. Nothing is withheld that the row otherwise granted. + * + * `label`, `sizeBytes` and `progress` are decoration behind main's own guards — `!bytes`, + * `progress != null` — so a salvaged member lands on exactly the meta string main drew. + * `recommended` stays `z.unknown()` because the badge gates on truthiness, not on `=== true`. + */ +const speechModelSchema = z.looseObject({ + id: z.string(), + label: salvagedOptional('label', z.string()), + provider: salvagedOptional('provider', z.enum(SPEECH_MODEL_PROVIDERS)), + status: salvagedOptional('status', z.enum(SPEECH_MODEL_STATUSES)), + sizeBytes: salvagedOptional('sizeBytes', z.number().nullable()), + progress: salvagedOptional('progress', z.number().nullable()), + recommended: z.unknown().optional() +}) + +/** + * The whole dictation setup, which all three of the list, the delete and the config write answer + * with — the sheet renders the reply in place of a refetch, so one schema covers the three. + * + * `models` is required: MobileDictationSetupSheet.tsx:49 and VoiceModelList.tsx:53 read `.some` and + * `.map` on it with no guard, so a reply without one was a TypeError inside the sheet's own refresh. + * + * `dictationMode` is forwarded as the string the host sent, not as an arm set. Every consumer is an + * equality test against `toggle` or `hold` — the voice-settings segments, the terminal input mic and + * the native-chat composer — so a mode this build has not heard of renders the same inert control + * whether it arrives verbatim or as a substitute, and forwarding is the one that makes no version + * claim. It is NOT required even though the host declares it so: main rendered a sheet without one, + * and an absent mode stays absent here so the mic it feeds is as inert as main's was. + * + * `enabled` and `selectedModelId` stay salvaged: both are read behind `!`/`===` and a reply missing + * either renders an off switch and no selected row, which is what main rendered for the same reply. + */ +export const dictationSetupSchema = z.looseObject({ + enabled: salvagedOptional('enabled', z.boolean()), + selectedModelId: salvagedOptional('selectedModelId', z.string()), + dictationMode: salvagedOptional('dictationMode', z.string()), + models: salvagingArray(speechModelSchema) +}) + +export type MobileSpeechSetupReply = z.output +export type MobileSpeechModelReply = MobileSpeechSetupReply['models'][number] + +/** + * The five dictation sends whose reply body no call site reads. + * + * `speech.models.download` answers `{ started: true }` and the sheet polls the list instead; the + * start, chunk and cancel replies are interpreted for their acceptance verdict alone. Declaring a + * member on any of them would be a requirement with no reader behind it. + * + * `speech.dictation.finish` is here for a different reason, and it is the one site in this domain + * left deliberately unchecked. Its transcript is read at the call site through `rpcPayloadMember` + * (use-mobile-dictation.ts:237) *after* a staleness guard (:225), and the interpretation that a + * schema would fail runs before that guard. Checking it would report an unreadable reply for a + * dictation + * the user had already superseded, where main returned silently; the member read itself is guarded + * by `typeof transcript === 'string'` and is fenced by the raw-port inventory. + */ +export const dictationUnreadReplySchema = z.unknown() diff --git a/mobile/src/dictation/mobile-dictation-operations.ts b/mobile/src/dictation/mobile-dictation-operations.ts index b3471808c74..7cf1d96666b 100644 --- a/mobile/src/dictation/mobile-dictation-operations.ts +++ b/mobile/src/dictation/mobile-dictation-operations.ts @@ -1,9 +1,16 @@ import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' -import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' +import { rpcResultVariant } from '../transport/rpc-operation-result-reader' +import { dictationSetupSchema, dictationUnreadReplySchema } from './dictation-reply-schema' // The dictation setup sheet's reads and writes, and the three sends one dictation session makes. // Every refusing site here surfaces the host's own message with a screen fallback, so they share // one policy and differ only in the copy they fall back to, which stays at the call site. +// +// Three of the eight read a setup the sheet renders, and those are checked. The other five read no +// reply body at all, or read it past a guard whose order is load-bearing; dictation-reply-schema.ts +// says which and why. An unreadable setup now reaches the sheet's own catch through +// `interpretOrThrowRefusalMessage`, which shows the host-reply message where main showed +// `undefined` models and then crashed the refresh on `.some`. export const dictationSetupRead = bindDeferredRpcOperation( defineRpcOperation({ @@ -11,7 +18,7 @@ export const dictationSetupRead = bindDeferredRpcOperation( method: 'speech.models.list', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('dictation-setup') + read: rpcResultVariant('dictation-setup', dictationSetupSchema) }) ) @@ -22,7 +29,7 @@ export const dictationModelDownload = bindDeferredRpcOperation( method: 'speech.models.download', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('dictation-download-started') + read: rpcResultVariant('dictation-download-started', dictationUnreadReplySchema) }) ) @@ -33,7 +40,7 @@ export const dictationModelDelete = bindDeferredRpcOperation( method: 'speech.models.delete', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('dictation-setup') + read: rpcResultVariant('dictation-setup', dictationSetupSchema) }) ) @@ -43,7 +50,7 @@ export const dictationConfigWrite = bindDeferredRpcOperation( method: 'speech.dictation.setup', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('dictation-setup') + read: rpcResultVariant('dictation-setup', dictationSetupSchema) }) ) @@ -53,7 +60,7 @@ export const dictationSessionStart = bindDeferredRpcOperation( method: 'speech.dictation.start', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('dictation-started') + read: rpcResultVariant('dictation-started', dictationUnreadReplySchema) }) ) @@ -63,7 +70,7 @@ export const dictationAudioChunkSend = bindDeferredRpcOperation( method: 'speech.dictation.chunk', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('dictation-chunk-received') + read: rpcResultVariant('dictation-chunk-received', dictationUnreadReplySchema) }) ) @@ -78,7 +85,7 @@ export const dictationSessionFinish = bindDeferredRpcOperation( method: 'speech.dictation.finish', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('dictation-finished') + read: rpcResultVariant('dictation-finished', dictationUnreadReplySchema) }) ) @@ -94,6 +101,6 @@ export const dictationSessionCancel = bindDeferredRpcOperation( method: 'speech.dictation.cancel', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('dictation-cancelled') + read: rpcResultVariant('dictation-cancelled', dictationUnreadReplySchema) }) ) diff --git a/mobile/src/dictation/mobile-dictation-setup.test.ts b/mobile/src/dictation/mobile-dictation-setup.test.ts index f84e545af49..a732593f14f 100644 --- a/mobile/src/dictation/mobile-dictation-setup.test.ts +++ b/mobile/src/dictation/mobile-dictation-setup.test.ts @@ -62,14 +62,24 @@ describe('isDictationSetupRequiredError', () => { describe('rpc wrappers', () => { it('fetches setup', async () => { - const setup: MobileSpeechSetup = { enabled: false, selectedModelId: '', models: [] } + const setup: MobileSpeechSetup = { + enabled: false, + selectedModelId: '', + dictationMode: 'toggle', + models: [] + } const client = clientWith([ok(setup)]) await expect(fetchDictationSetup(client)).resolves.toEqual(setup) expect(client.calls[0]).toEqual({ method: 'speech.models.list', params: null }) }) it('retries the idempotent setup read once after logical-client cutover', async () => { - const setup: MobileSpeechSetup = { enabled: false, selectedModelId: '', models: [] } + const setup: MobileSpeechSetup = { + enabled: false, + selectedModelId: '', + dictationMode: 'toggle', + models: [] + } const sendRequest = vi .fn() .mockRejectedValueOnce(new LogicalClientCutoverError()) @@ -95,14 +105,24 @@ describe('rpc wrappers', () => { }) it('deletes a model and returns refreshed setup', async () => { - const setup: MobileSpeechSetup = { enabled: true, selectedModelId: '', models: [] } + const setup: MobileSpeechSetup = { + enabled: true, + selectedModelId: '', + dictationMode: 'toggle', + models: [] + } const client = clientWith([ok(setup)]) await expect(deleteDictationModel(client, 'm1')).resolves.toEqual(setup) expect(client.calls[0]).toEqual({ method: 'speech.models.delete', params: { modelId: 'm1' } }) }) it('sets config', async () => { - const setup: MobileSpeechSetup = { enabled: true, selectedModelId: 'm1', models: [] } + const setup: MobileSpeechSetup = { + enabled: true, + selectedModelId: 'm1', + dictationMode: 'hold', + models: [] + } const client = clientWith([ok(setup)]) await expect(setDictationConfig(client, { enabled: true, modelId: 'm1' })).resolves.toEqual( setup @@ -181,6 +201,7 @@ describe('state helpers', () => { isDictationReady({ enabled: true, selectedModelId: 'm1', + dictationMode: 'toggle', models: [model({ status: 'ready' })] }) ).toBe(true) @@ -188,6 +209,7 @@ describe('state helpers', () => { isDictationReady({ enabled: false, selectedModelId: 'm1', + dictationMode: 'toggle', models: [model({ status: 'ready' })] }) ).toBe(false) @@ -195,9 +217,17 @@ describe('state helpers', () => { isDictationReady({ enabled: true, selectedModelId: 'm1', + dictationMode: 'toggle', models: [model({ status: 'not-downloaded' })] }) ).toBe(false) - expect(isDictationReady({ enabled: true, selectedModelId: '', models: [] })).toBe(false) + expect( + isDictationReady({ + enabled: true, + selectedModelId: '', + dictationMode: 'toggle', + models: [] + }) + ).toBe(false) }) }) diff --git a/mobile/src/dictation/mobile-dictation-setup.ts b/mobile/src/dictation/mobile-dictation-setup.ts index 9ac8aae5d21..6f3002bea8c 100644 --- a/mobile/src/dictation/mobile-dictation-setup.ts +++ b/mobile/src/dictation/mobile-dictation-setup.ts @@ -1,4 +1,3 @@ -import type { RuntimeSpeechSetupState } from '../../../src/shared/runtime-types' import type { RpcClient } from '../transport/rpc-client' import type { RpcResponse } from '../transport/types' import { interpretOrThrowRefusalMessage } from '../transport/rpc-refusal-message' @@ -9,9 +8,12 @@ import { dictationModelDownload, dictationSetupRead } from './mobile-dictation-operations' +import type { MobileSpeechModelReply, MobileSpeechSetupReply } from './dictation-reply-schema' -export type MobileSpeechSetup = RuntimeSpeechSetupState -export type MobileSpeechModel = RuntimeSpeechSetupState['models'][number] +// The setup as the reply reader hands it back, not RuntimeSpeechSetupState: the reader salvages the +// members the sheet reads behind a guard, so the screens see what it actually checked. +export type MobileSpeechSetup = MobileSpeechSetupReply +export type MobileSpeechModel = MobileSpeechModelReply // Dictation-setup errors startMobileDictation throws when the desktop isn't // configured. Mapping them lets the mic entry point open the setup sheet @@ -45,11 +47,10 @@ export async function fetchDictationSetup(client: RpcClient): Promise dictationSetupRead.interpret(reply), 'Failed to load dictation models' - ) as MobileSpeechSetup + ) } async function requestDictationSetupReply(client: RpcClient): Promise { @@ -78,11 +79,10 @@ export async function deleteDictationModel( modelId: string ): Promise { const reply = await dictationModelDelete.request(client, { modelId }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. return interpretOrThrowRefusalMessage( () => dictationModelDelete.interpret(reply), 'Failed to delete model' - ) as MobileSpeechSetup + ) } export async function setDictationConfig( @@ -90,11 +90,10 @@ export async function setDictationConfig( params: { enabled?: boolean; modelId?: string; dictationMode?: 'toggle' | 'hold' } ): Promise { const reply = await dictationConfigWrite.request(client, params) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. return interpretOrThrowRefusalMessage( () => dictationConfigWrite.interpret(reply), 'Failed to update dictation settings' - ) as MobileSpeechSetup + ) } // A model is mid-download (or extracting) and the sheet should keep polling. diff --git a/mobile/src/files/MobileFileExplorerPanel.tsx b/mobile/src/files/MobileFileExplorerPanel.tsx index 619bfd5c4a2..84b05e0fb43 100644 --- a/mobile/src/files/MobileFileExplorerPanel.tsx +++ b/mobile/src/files/MobileFileExplorerPanel.tsx @@ -16,8 +16,7 @@ import { flattenDirectoryCache, getDirectoryCacheState, type DirectoryCache, - type FileExplorerRow, - type MobileDirEntry + type FileExplorerRow } from './file-tree' import type { RpcFailure } from '../transport/types' import { colors } from '../theme/mobile-theme' @@ -28,11 +27,7 @@ import { resetDirectoryLoadRevisions, type DirectoryLoadRevisions } from './directory-load-revisions' -import { - directoryCacheFromFileList, - isMobileMethodUnavailableError, - type LegacyFilesListResult -} from './file-list-fallback' +import { directoryCacheFromFileList, isMobileMethodUnavailableError } from './file-list-fallback' import { fileDirectoryRead, legacyFileListRead } from './mobile-file-explorer-operations' import { fileExplorerStyles as styles } from './mobile-file-explorer-styles' import { MobileFileExplorerRow } from './mobile-file-explorer-row' @@ -134,8 +129,7 @@ export function MobileFileExplorerPanel(props: { ) { return } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const legacyResult = legacy.value as LegacyFilesListResult + const legacyResult = legacy.value setDirectoryCache(directoryCacheFromFileList(legacyResult.files)) // Why: the capped list silently omits files past the cap — keep // the legacy explorer's "Showing first 5000" note. @@ -156,8 +150,7 @@ export function MobileFileExplorerPanel(props: { ) { return } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const entries = directory.value as MobileDirEntry[] + const entries = directory.value if (rootLoad) { setLegacyListTruncated(false) } diff --git a/mobile/src/files/file-explorer-reply-schema.test.ts b/mobile/src/files/file-explorer-reply-schema.test.ts new file mode 100644 index 00000000000..362d2d7f657 --- /dev/null +++ b/mobile/src/files/file-explorer-reply-schema.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import { fileDirectoryEntriesSchema, legacyFileListSchema } from './file-explorer-reply-schema' + +describe('file explorer reply schemas', () => { + it('requires the listing itself to be an array', () => { + expect(fileDirectoryEntriesSchema.safeParse({ entries: [] }).success).toBe(false) + expect(fileDirectoryEntriesSchema.safeParse([]).success).toBe(true) + }) + + it('drops a row the tree cannot place and keeps the rest of the directory', () => { + const parsed = fileDirectoryEntriesSchema.parse([ + { name: 'src', isDirectory: true }, + { isDirectory: false }, + { name: 'readme.md' }, + { name: 'main.ts', isDirectory: false, isSymlink: true } + ]) + expect(parsed.map((entry) => entry.name)).toEqual(['src', 'main.ts']) + }) + + it('salvages isSymlink without dropping the row', () => { + expect( + fileDirectoryEntriesSchema.parse([{ name: 'a', isDirectory: false, isSymlink: 'yes' }])[0] + ?.isSymlink + ).toBeUndefined() + }) + + it('requires the capped list and the note it draws', () => { + const listed = { files: [], truncated: true } + expect(legacyFileListSchema.safeParse(listed).success).toBe(true) + // One at a time, so a sibling requirement cannot stand in for the member under test. + for (const member of ['files', 'truncated'] as const) { + const { [member]: _dropped, ...without } = listed + expect(legacyFileListSchema.safeParse(without).success).toBe(false) + } + }) + + it('drops a legacy row that names no path and keeps the rest', () => { + const parsed = legacyFileListSchema.parse({ + files: [{ relativePath: 'a/b.ts' }, { basename: 'c.ts' }, { relativePath: 4 }], + truncated: false + }) + expect(parsed.files).toEqual([{ relativePath: 'a/b.ts' }]) + }) + + it('passes a newer host member through on a directory row', () => { + expect( + fileDirectoryEntriesSchema.parse([{ name: 'a', isDirectory: false, sizeBytes: 12 }])[0] + ).toMatchObject({ sizeBytes: 12 }) + }) +}) diff --git a/mobile/src/files/file-explorer-reply-schema.ts b/mobile/src/files/file-explorer-reply-schema.ts new file mode 100644 index 00000000000..696bdde55a1 --- /dev/null +++ b/mobile/src/files/file-explorer-reply-schema.ts @@ -0,0 +1,45 @@ +import { z } from 'zod' +import { salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage' + +// The Files tab's directory read and the capped list it falls back to. Checked against +// src/main/runtime/rpc/methods/files.ts:125 and :27, and the shared results they return: +// DirEntry rows from readFileExplorerDir, RuntimeFileListResult from listMobileFiles. + +/** + * One directory's entries. + * + * The payload is the array itself, and it is required: MobileFileExplorerPanel.tsx:157 puts it + * straight into the directory cache, where flattenDirectoryCache (file-tree.ts:58) sorts and walks + * it — a reply that was not an array was a `.filter` on a string one render later, with nothing + * naming the reply. + * + * A row needs `name` and `isDirectory`, and a row without either drops rather than failing the + * whole read, which is what a skip policy means for a directory listing: compareFileNames reads + * `name` unguarded, and `isDirectory` is the discriminator the whole tree projection turns on, so a + * row without it is a file that can never be opened and whose label renders `undefined`. + * `isSymlink` is decoration on the row and stays salvaged. + */ +export const fileDirectoryEntriesSchema = salvagingArray( + z.looseObject({ + name: z.string(), + isDirectory: z.boolean(), + isSymlink: salvagedOptional('isSymlink', z.boolean()) + }) +) + +/** + * The capped flat list an older desktop answers when `files.readDir` is not allowlisted. + * + * `files` and `truncated` are both required and both read unguarded: directoryCacheFromFileList + * walks `files` and splits each `relativePath` (file-list-fallback.ts:48), and + * MobileFileExplorerPanel.tsx:136 publishes `truncated` into the state that draws the "Showing + * first 5000" note. A row without a string `relativePath` drops — it can name no directory — where + * main crashed the whole fallback on it. + * + * `basename` and `kind` are not declared: this consumer reads neither, and passthrough keeps them + * for the inventory reader that does. + */ +export const legacyFileListSchema = z.looseObject({ + files: salvagingArray(z.looseObject({ relativePath: z.string() })), + truncated: z.boolean() +}) diff --git a/mobile/src/files/file-list-fallback.ts b/mobile/src/files/file-list-fallback.ts index aa7323e13b5..e0db7236bad 100644 --- a/mobile/src/files/file-list-fallback.ts +++ b/mobile/src/files/file-list-fallback.ts @@ -29,7 +29,11 @@ export function isMobileMethodUnavailableError( ) } -export function directoryCacheFromFileList(files: LegacyMobileFileEntry[]): DirectoryCache { +// Takes only the member it reads: the explorer's reply reader checks `relativePath` and passes the +// rest of each row through, so naming the whole row here would re-declare what it deliberately did not. +export function directoryCacheFromFileList( + files: readonly { relativePath: string; [member: string]: unknown }[] +): DirectoryCache { const childrenByDir = new Map>() const ensureDir = (path: string): Map => { let children = childrenByDir.get(path) diff --git a/mobile/src/files/file-ownership-reply-schema.test.ts b/mobile/src/files/file-ownership-reply-schema.test.ts new file mode 100644 index 00000000000..33930873217 --- /dev/null +++ b/mobile/src/files/file-ownership-reply-schema.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest' +import { + fileOwnershipSshStateSchema, + fileOwnershipWorktreeSchema +} from './file-ownership-reply-schema' + +describe('file ownership reply schemas', () => { + it('keeps the three hostId states distinct', () => { + // Absent means no host recorded and captures local; explicit null means the host said local; + // a string is parsed. Collapsing absent and null would change where a write lands. + expect(fileOwnershipWorktreeSchema.parse({ worktree: {} })?.hostId).toBeUndefined() + expect(fileOwnershipWorktreeSchema.parse({ worktree: { hostId: null } })?.hostId).toBeNull() + expect(fileOwnershipWorktreeSchema.parse({ worktree: { hostId: 'ssh:a' } })?.hostId).toBe( + 'ssh:a' + ) + }) + + it('refuses a wrong-typed hostId instead of salvaging it to local', () => { + expect(fileOwnershipWorktreeSchema.safeParse({ worktree: { hostId: 7 } }).success).toBe(false) + }) + + it('keeps an unresolved worktree readable as the absent summary main threw on', () => { + expect(fileOwnershipWorktreeSchema.parse({})).toBeUndefined() + expect(fileOwnershipWorktreeSchema.parse({ worktree: null })).toBeNull() + }) + + it('passes the connection generation through and refuses a wrong-typed one', () => { + // The generation is echoed back to the host on the mutation, so a client-side fallback here + // would put a value on the wire the host then refuses. + expect( + fileOwnershipSshStateSchema.parse({ state: { targetId: 't', connectionGeneration: 3 } }) + ?.connectionGeneration + ).toBe(3) + expect( + fileOwnershipSshStateSchema.safeParse({ state: { targetId: 't', connectionGeneration: '3' } }) + .success + ).toBe(false) + }) + + it('salvages a wrong-typed targetId onto the mismatch main threw', () => { + expect( + fileOwnershipSshStateSchema.parse({ state: { targetId: 7, connectionGeneration: 1 } }) + ?.targetId + ).toBeUndefined() + }) + + it('reads a host holding no connection as the null state it sends', () => { + expect(fileOwnershipSshStateSchema.parse({ state: null })).toBeNull() + expect(fileOwnershipSshStateSchema.parse({})).toBeUndefined() + }) +}) diff --git a/mobile/src/files/file-ownership-reply-schema.ts b/mobile/src/files/file-ownership-reply-schema.ts new file mode 100644 index 00000000000..aa6d3915bcf --- /dev/null +++ b/mobile/src/files/file-ownership-reply-schema.ts @@ -0,0 +1,53 @@ +import { z } from 'zod' +import { salvagedOptional } from '../../../src/shared/zod-salvage' + +// The two reads that pin which execution host owns a workspace before a file mutation is sent. +// Checked against src/main/runtime/rpc/methods/worktree.ts:48 and ssh.ts:30, and the shared +// SshConnectionState in src/shared/ssh-types.ts:187. +// +// This capture decides where a write lands, so the usual "degrade to absent" salvage is wrong for +// the two members it routes on: absent reads as *local* downstream, and turning an unreadable owner +// into a local one is how a mutation reaches the wrong host. Both are declared fatal instead. + +/** + * The workspace row a mutation targets. + * + * The payload wrapper stays nullish so an absent `worktree` still reaches + * mobile-file-mutation-ownership.ts:68 as the `!summary` throw main had, rather than as a decode + * failure — the host answers `{ worktree: undefined }` for a selector it cannot resolve. + * + * `hostId` is a plain nullable optional, not a salvaged one, and the distinction is load-bearing + * twice over. Its three states are distinct to buildMobileFileMutationOwnership: absent means "no + * host recorded" and yields a local capture, `null` means the reply named a host this client cannot + * place and is refused (mobile-file-mutation-ownership.ts:32, pinned at its test:122), and a string + * is parsed. A salvage would fold a *wrong-typed* hostId into absent and let the mutation go local; + * main threw "Couldn't verify the SSH connection" on it, and an incompatible reply throws too. + */ +export const fileOwnershipWorktreeSchema = z + .looseObject({ + worktree: z.looseObject({ hostId: z.string().nullable().optional() }).nullish() + }) + .transform((reply) => reply.worktree) + +/** + * The SSH connection generation the mutation is expected to still be running on. + * + * `connectionGeneration` is echoed back to the host as `expectedSshConnectionGeneration` on the + * mutation itself, so it is passed through at its own type and a wrong type is fatal: a reply-side + * fallback here would put a value on the wire that the host then refuses, and main's + * `=== undefined` check would have let a non-number through unnoticed. + * + * `targetId` is only ever compared against the parsed host's own target, so a salvaged member lands + * on exactly main's mismatch throw. The `state` member itself is nullish because the host answers + * `{ state: null }` for a target it holds no connection for, which is the normal local case. + */ +export const fileOwnershipSshStateSchema = z + .looseObject({ + state: z + .looseObject({ + targetId: salvagedOptional('targetId', z.string()), + connectionGeneration: z.number().optional() + }) + .nullish() + }) + .transform((reply) => reply.state) diff --git a/mobile/src/files/file-preview-reply-schema.test.ts b/mobile/src/files/file-preview-reply-schema.test.ts new file mode 100644 index 00000000000..4810c5688ad --- /dev/null +++ b/mobile/src/files/file-preview-reply-schema.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest' +import { + filePreviewImageSchema, + filePreviewTextSchema, + terminalPathResolutionSchema +} from './file-preview-reply-schema' + +describe('file preview reply schemas', () => { + it('requires the content the markdown disk fallback publishes unguarded', () => { + // `content` alone, with no sibling requirement able to stand in for it. + expect(filePreviewTextSchema.safeParse({ truncated: false, byteLength: 0 }).success).toBe(false) + expect(filePreviewTextSchema.safeParse({ content: '# readme' }).success).toBe(true) + expect(filePreviewTextSchema.safeParse({ content: 7 }).success).toBe(false) + }) + + it('salvages truncated and byteLength onto the fallbacks main already had', () => { + const parsed = filePreviewTextSchema.parse({ + content: 'body', + truncated: 'yes', + byteLength: 'four' + }) + // Absent reads as not truncated, and an unreadable byteLength falls through to content.length + // at the call site, which is what main's `typeof === 'number'` guard already did. + expect(parsed.truncated).toBeUndefined() + expect(parsed.byteLength).toBeUndefined() + }) + + it('keeps every member of an image preview optional so the host binary arms still render', () => { + // The host answers this shape for a binary it cannot preview, and this one for a path mobile + // classified as an image and the host did not. Both reach the screen as main's own copy. + expect(filePreviewImageSchema.safeParse({ content: '', isBinary: true }).success).toBe(true) + expect(filePreviewImageSchema.safeParse({ content: 'text', isBinary: false }).success).toBe( + true + ) + }) + + it('salvages an image preview member to the arm main fell back to', () => { + const parsed = filePreviewImageSchema.parse({ + content: 'aGk=', + isImage: 'yes', + mimeType: 7 + }) + expect(parsed.isImage).toBeUndefined() + expect(parsed.mimeType).toBeUndefined() + }) + + it('refuses a preview payload that is not an object', () => { + expect(filePreviewTextSchema.safeParse('# readme').success).toBe(false) + expect(filePreviewImageSchema.safeParse(null).success).toBe(false) + }) + + it('keeps a terminal path resolution readable with every member salvaged', () => { + const parsed = terminalPathResolutionSchema.parse({ + exists: 'yes', + isDirectory: false, + openTarget: { kind: 'absolute-file', absolutePath: '/logs/run.txt', grantId: 'g2' } + }) + expect(parsed.exists).toBeUndefined() + expect(parsed.openTarget?.grantId).toBe('g2') + }) + + it('passes a newer host member through on every preview schema', () => { + expect(filePreviewTextSchema.parse({ content: 'a', encoding: 'utf8' })).toMatchObject({ + encoding: 'utf8' + }) + expect( + filePreviewImageSchema.parse({ content: 'a', imageDimensions: { width: 1 } }) + ).toMatchObject({ imageDimensions: { width: 1 } }) + }) +}) diff --git a/mobile/src/files/file-preview-reply-schema.ts b/mobile/src/files/file-preview-reply-schema.ts new file mode 100644 index 00000000000..8c35df8eb39 --- /dev/null +++ b/mobile/src/files/file-preview-reply-schema.ts @@ -0,0 +1,92 @@ +import { z } from 'zod' +import { salvagedOptional } from '../../../src/shared/zod-salvage' + +// What the preview screen reads for one file, and what the grant refresh reads to re-mint a stale +// grant. Checked against the four handlers in src/main/runtime/rpc/methods/files.ts:70/104 and +// files-terminal-artifact-methods.ts:10/21/42, and the shared results they return verbatim: +// RuntimeFileReadResult, RuntimeFilePreviewResult and RuntimeTerminalPathResolution in +// src/shared/runtime-file-contracts.ts. +// +// Three encodings are used here and mean three different things, so they are stated once: +// - a required member is one a consumer reads with no guard, where absence renders `undefined` +// or throws on the next property; +// - `salvagedOptional(name, T)` is for a member behind a *typed* guard with a fallback — the +// salvage lands on exactly that fallback, so the screen shows what main showed; +// - `z.unknown()` is for a member read only for truthiness, because narrowing it would move +// main's answer for a value the consumer's own guard already accepted. + +/** + * A file's text, for `files.read` and `files.readTerminalArtifact` alike: one host result type, and + * the preview screen normalizes both through the same projection. + * + * `content` is required because the markdown disk fallback reads it with no guard — + * use-mobile-session-document-readers.ts:60 publishes it straight into the tab's ready document, so + * a reply without one rendered `undefined` in the editor. The preview screen's own reader guards it + * (`typeof preview.content !== 'string'` in mobile-file-preview-response.ts:142) and lands on + * 'Unable to load preview', which is the same copy `previewError` gives the incompatible-reply + * message — so requiring it moves the preview screen's text not at all. + * + * `truncated` and `byteLength` stay salvaged. Both are declared required by RuntimeFileReadResult, + * but neither can crash or render garbage: `truncated` is a truthiness test behind a read-only + * reason string and `byteLength` has main's own `preview.content.length` fallback behind a + * `typeof === 'number'` guard. Requiring either would only let a host that trims a field take the + * whole preview down. `isBinary` is never sent on these two methods — the host raises `binary_file` + * instead — but the projection still checks it, so it is declared where main looked for it. + */ +export const filePreviewTextSchema = z.looseObject({ + content: z.string(), + truncated: salvagedOptional('truncated', z.boolean()), + byteLength: salvagedOptional('byteLength', z.number()), + isBinary: salvagedOptional('isBinary', z.boolean()) +}) + +/** + * A file's image bytes, for `files.readPreview` and `files.readTerminalArtifactPreview`. + * + * Nothing is required: normalizeImagePreviewResult guards all four members and falls back to + * `previewError('binary_file')` for every one of them, and that fallback is load-bearing — the host + * answers `{ content, isBinary: true }` with no mime for a binary it cannot preview, and + * `{ content, isBinary: false }` for a path mobile classifies as an image and the host does not. + * Both are good replies the screen renders today. The guards are all `=== true` / `!== true` / + * `typeof === 'string'`, so a salvaged member lands on exactly the arm main took. + * + * What the schema adds is the container. A bare string or a null result reached the projection as + * 'Binary preview unavailable', which names the file rather than the reply. + */ +export const filePreviewImageSchema = z.looseObject({ + content: salvagedOptional('content', z.string()), + isBinary: salvagedOptional('isBinary', z.boolean()), + isImage: salvagedOptional('isImage', z.boolean()), + mimeType: salvagedOptional('mimeType', z.string()) +}) + +/** + * A terminal path re-resolved to mint a fresh grant. + * + * Nothing is required and every member is salvaged: isTerminalArtifactResolution + * (mobile-terminal-artifact-grant-refresh.ts:77) is a total guard that answers "not refreshable" + * for anything it cannot read, and a refusal to refresh is a normal outcome rather than an error. + * The schema declares the members that guard reads so a newer host's extra keys pass through, and + * adds only the container. + */ +export const terminalPathResolutionSchema = z.looseObject({ + exists: salvagedOptional('exists', z.boolean()), + isDirectory: salvagedOptional('isDirectory', z.boolean()), + openTarget: salvagedOptional( + 'openTarget', + z.looseObject({ + kind: salvagedOptional('kind', z.string()), + absolutePath: salvagedOptional('absolutePath', z.string()), + grantId: salvagedOptional('grantId', z.string()), + readOnly: salvagedOptional('readOnly', z.literal(true)) + }) + ) +}) + +/** + * The artifact save's reply body, which no call site reads. + * + * `writeTerminalArtifactFile` answers `{ ok: true }` and settlePreviewSend looks only at the + * acceptance verdict, so declaring a member would be a requirement with no reader behind it. + */ +export const terminalArtifactWriteSchema = z.unknown() diff --git a/mobile/src/files/file-tab-doc-reply-schema.test.ts b/mobile/src/files/file-tab-doc-reply-schema.test.ts new file mode 100644 index 00000000000..f561416b262 --- /dev/null +++ b/mobile/src/files/file-tab-doc-reply-schema.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest' +import { + fileTabBinaryDiffSchema, + fileTabImageSchema, + fileTabTextDiffSchema, + fileTabTextSchema +} from './file-tab-doc-reply-schema' + +describe('file tab doc reply schemas', () => { + it('requires the three members the tab publishes into a ready document', () => { + const ready = { content: 'a', truncated: false, byteLength: 1 } + expect(fileTabTextSchema.safeParse(ready).success).toBe(true) + // One at a time, so a sibling requirement cannot stand in for the member under test. + for (const member of ['content', 'truncated', 'byteLength'] as const) { + const { [member]: _dropped, ...without } = ready + expect(fileTabTextSchema.safeParse(without).success).toBe(false) + } + }) + + it('requires the image content buildImageDataUri calls replace on', () => { + expect(fileTabImageSchema.safeParse({ isImage: true, mimeType: 'image/png' }).success).toBe( + false + ) + }) + + it('keeps an image tab readable for the truthy non-boolean isImage main rendered', () => { + expect(fileTabImageSchema.parse({ content: 'aGk=', isImage: 1 }).isImage).toBe(1) + }) + + it('refuses a wrong-typed mimeType rather than salvaging it', () => { + // Main threw on `mimeType?.startsWith`, and readFileTab's catch showed + // "Couldn't load file preview"; salvaging to absent would have shown the binary copy instead. + expect(fileTabImageSchema.safeParse({ content: 'aGk=', mimeType: 7 }).success).toBe(false) + }) + + it('reads a text diff only when both sides are strings', () => { + expect( + fileTabTextDiffSchema.safeParse({ kind: 'text', originalContent: 'a', modifiedContent: 'b' }) + .success + ).toBe(true) + expect(fileTabTextDiffSchema.safeParse({ kind: 'text', originalContent: 'a' }).success).toBe( + false + ) + }) + + it('routes an unknown diff arm to the binary reader and never the text one', () => { + expect(fileTabBinaryDiffSchema.parse({ kind: 'submodule' }).kind).toBe('binary') + expect(fileTabBinaryDiffSchema.parse({ kind: 'binary' }).kind).toBe('binary') + // A text diff whose contents did not arrive must not be re-read as binary: the tab would name + // the file unpreviewable instead of naming the reply. + expect(fileTabBinaryDiffSchema.safeParse({ kind: 'text' }).success).toBe(false) + }) + + it('salvages the binary arm members main compared against true', () => { + const parsed = fileTabBinaryDiffSchema.parse({ + kind: 'binary', + isImage: 'yes', + modifiedDeleted: 1, + modifiedContent: 5 + }) + expect(parsed.isImage).toBeUndefined() + expect(parsed.modifiedDeleted).toBeUndefined() + expect(parsed.modifiedContent).toBeUndefined() + }) + + it('passes a newer host member through', () => { + expect( + fileTabTextDiffSchema.parse({ + kind: 'text', + originalContent: 'a', + modifiedContent: 'b', + largeDiffRenderLimit: { lines: 10 } + }) + ).toMatchObject({ largeDiffRenderLimit: { lines: 10 } }) + }) +}) diff --git a/mobile/src/files/file-tab-doc-reply-schema.ts b/mobile/src/files/file-tab-doc-reply-schema.ts new file mode 100644 index 00000000000..e25f613b456 --- /dev/null +++ b/mobile/src/files/file-tab-doc-reply-schema.ts @@ -0,0 +1,96 @@ +import { z } from 'zod' +import { salvagedOptional } from '../../../src/shared/zod-salvage' + +// What a session file tab reads to render one document. Checked against +// src/main/runtime/rpc/methods/files.ts:70/104 and git-diff-methods.ts:16, and the shared results +// they return: RuntimeFileReadResult and RuntimeFilePreviewResult in runtime-file-contracts.ts, +// GitDiffTextResult / GitDiffBinaryResult in git-diff-compare-types.ts. +// +// A tab is stricter than the preview screen on the same two file methods, and that is a property of +// the consumer rather than of the host: resolveMobileFileTabDoc publishes what it reads straight +// into a typed ready document with no guard, where the preview screen normalizes every member. + +/** + * The text a file tab renders, from `files.read`. + * + * All three are required because all three are published unguarded into MobileFileTabDoc: + * mobile-file-tab-doc.ts:68 renders `content` as the html body and :73-75 puts `content`, + * `truncated` and `byteLength` into the `file` arm, whose size label and truncation banner read + * them as a number and a boolean. A reply missing one rendered `undefined` in the tab. All three + * are declared required by RuntimeFileReadResult, so no host that answers this method omits them. + */ +export const fileTabTextSchema = z.looseObject({ + content: z.string(), + truncated: z.boolean(), + byteLength: z.number() +}) + +/** + * The image bytes a file tab renders, from `files.readPreview`. + * + * `content` is required: buildImageDataUri runs `base64Content.replace` with no guard once + * `isImage` is truthy (mobile-file-tab-doc.ts:58), so a reply without a string content was a + * TypeError. RuntimeFilePreviewResult declares it required. + * + * `mimeType` is a plain optional rather than a salvaged one, because main had no fallback for a + * wrong type here either: `mimeType?.startsWith` threw on a non-string, and readFileTab's catch + * showed "Couldn't load file preview". An incompatible reply reaches that same catch with that same + * copy, where salvaging to absent would instead have shown 'Binary preview unavailable'. + * + * `isImage` stays `z.unknown()`: the tab gates on its truthiness, not on `=== true`, so narrowing + * it to a boolean would drop an image main rendered. + */ +export const fileTabImageSchema = z.looseObject({ + content: z.string(), + isImage: z.unknown().optional(), + mimeType: z.string().optional() +}) + +/** + * The text arm of `git.diff`: the only arm whose contents are read. + * + * Both sides are required because buildMobileDiffLines reads `content.length` on each with no + * guard (mobile-diff-lines.ts:35), so a text diff missing one was a TypeError caught as + * "Couldn't load diff preview". + */ +const fileTabTextDiffSchema = z.looseObject({ + kind: z.literal('text'), + originalContent: z.string(), + modifiedContent: z.string() +}) + +/** + * Every other arm of `git.diff`, including one this build has not heard of. + * + * The arm set is a wire surface, so an unknown `kind` degrades here rather than refusing the reply: + * mobile-file-tab-doc.ts:41 asks only `kind !== 'text'`, and an unknown arm took this branch on + * main too. `kind` is therefore any string but `text` — routing an unreadable *text* diff here + * instead would render "Binary preview unavailable" for a diff whose contents simply did not + * arrive, which names the file rather than the reply. + * + * Nothing in the arm is required: mobileDiffImageDataUri guards every member it reads + * (mobile-diff-image-preview.ts:22-33) and answers null — 'binary_file' — for anything it cannot + * use. `isImage` and `modifiedDeleted` are `=== true` comparisons, so a salvaged member lands on + * main's own arm; `mimeType` is a plain optional for the same reason the image tab's is. + * + * `kind` is admitted as any string but `text` and answered as `binary`, because that is the arm the + * client resolved rather than the token the host sent: no consumer forwards or renders it, and + * naming it `binary` is what lets the tab tell the two arms apart without re-testing the string. + */ +const fileTabBinaryDiffSchema = z.looseObject({ + kind: z + .string() + .refine((kind) => kind !== 'text', 'not the text arm') + .transform(() => 'binary' as const), + originalContent: salvagedOptional('originalContent', z.string()), + modifiedContent: salvagedOptional('modifiedContent', z.string()), + isImage: salvagedOptional('isImage', z.boolean()), + modifiedDeleted: salvagedOptional('modifiedDeleted', z.boolean()), + mimeType: z.string().optional() +}) + +export type MobileFileTabDiff = + | z.output + | z.output + +export { fileTabBinaryDiffSchema, fileTabTextDiffSchema } diff --git a/mobile/src/files/mobile-diff-image-preview.ts b/mobile/src/files/mobile-diff-image-preview.ts index 09d7cd2ef36..25d0244e111 100644 --- a/mobile/src/files/mobile-diff-image-preview.ts +++ b/mobile/src/files/mobile-diff-image-preview.ts @@ -3,8 +3,10 @@ import { buildImageDataUri } from '../../../src/shared/image-data-uri' // modifiedDeleted marks a proven deletion (modified side genuinely absent); an // empty modifiedContent alone can't, since a relay/SSH read failure also arrives // empty with modifiedIsBinary false. +// Kind is not named here: the reply reader admits any arm but `text`, so a host arm this build has +// not heard of reaches the same projection main's `kind !== 'text'` sent it to. export type MobileBinaryDiffResult = { - kind: 'binary' + kind?: string originalContent?: string modifiedContent?: string originalIsBinary?: boolean diff --git a/mobile/src/files/mobile-file-explorer-operations.ts b/mobile/src/files/mobile-file-explorer-operations.ts index d325fbea245..835243df950 100644 --- a/mobile/src/files/mobile-file-explorer-operations.ts +++ b/mobile/src/files/mobile-file-explorer-operations.ts @@ -1,5 +1,6 @@ import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' -import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' +import { rpcResultVariant } from '../transport/rpc-operation-result-reader' +import { fileDirectoryEntriesSchema, legacyFileListSchema } from './file-explorer-reply-schema' /** * The Files tab's directory read and the capped list it falls back to. @@ -9,6 +10,10 @@ import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' * answers `method_not_found`), and the `files.list` refusal supplies the message the screen shows. * No acceptance policy exposes a refusal code, so the panel reads the envelope's own error the way * `mobile-file-preview-operations.ts` does, and only these two consumers want one. + * + * A malformed *accepted* reply is what moves: the panel's own catch already turns a throw into the + * inline directory error it drew for a failed load, so an unreadable listing now names the reply + * instead of crashing the row builder one render later. */ export const fileDirectoryRead = bindDeferredRpcOperation( defineRpcOperation({ @@ -16,7 +21,7 @@ export const fileDirectoryRead = bindDeferredRpcOperation( method: 'files.readDir', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('directory-entries') + read: rpcResultVariant('directory-entries', fileDirectoryEntriesSchema) }) ) @@ -33,6 +38,6 @@ export const legacyFileListRead = bindDeferredRpcOperation( method: 'files.list', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('legacy-file-list') + read: rpcResultVariant('legacy-file-list', legacyFileListSchema) }) ) diff --git a/mobile/src/files/mobile-file-mutation-ownership.test.ts b/mobile/src/files/mobile-file-mutation-ownership.test.ts index f3e98dca9c6..84b4d077063 100644 --- a/mobile/src/files/mobile-file-mutation-ownership.test.ts +++ b/mobile/src/files/mobile-file-mutation-ownership.test.ts @@ -105,6 +105,31 @@ describe('mobile file mutation ownership', () => { ]) }) + // The three hostId states the reply reader keeps distinct, read end to end. An absent host is + // "none recorded" and captures local; an explicit null is a host that named something this client + // cannot place, and it refuses rather than sending the write to the runtime-local host. + it('captures local ownership for a workspace whose reply records no host', async () => { + const { client } = clientWithResponses([ + success({ capabilities: [FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY] }), + success({ worktree: {} }) + ]) + + await expect(captureMobileFileMutationOwnership(client, 'id:worktree-1')).resolves.toEqual({ + expectedExecutionHostId: 'local' + }) + }) + + it('refuses a workspace whose reply names an explicit null host', async () => { + const { client } = clientWithResponses([ + success({ capabilities: [FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY] }), + success({ worktree: { hostId: null } }) + ]) + + await expect(captureMobileFileMutationOwnership(client, 'id:worktree-1')).rejects.toThrow( + "Couldn't verify the SSH connection" + ) + }) + it('refuses older runtimes before reading or mutating workspace files', async () => { const { client, sendRequest } = clientWithResponses([success({ capabilities: [] })]) diff --git a/mobile/src/files/mobile-file-mutation-ownership.ts b/mobile/src/files/mobile-file-mutation-ownership.ts index 634f90f0637..9384df7801c 100644 --- a/mobile/src/files/mobile-file-mutation-ownership.ts +++ b/mobile/src/files/mobile-file-mutation-ownership.ts @@ -16,9 +16,16 @@ export type MobileFileMutationOwnership = SshMutationExpectation & { expectedExecutionHostId: 'local' | `ssh:${string}` } +// The two members the ownership gate routes on, as the reply reader hands them back. Absence and +// an explicit `null` stay distinct: the host omits `state` for a target it holds no connection for. +export type MobileFileMutationSshState = + | (Pick & { targetId?: string }) + | null + | undefined + export function buildMobileFileMutationOwnership( worktreeHostId: string | null | undefined, - sshState: SshConnectionState | null = null + sshState: MobileFileMutationSshState = null ): MobileFileMutationOwnership { const host = parseExecutionHostId(worktreeHostId) if (worktreeHostId !== undefined && !host) { @@ -52,24 +59,20 @@ export async function captureMobileFileMutationOwnership( { worktree }, { timeoutMs: FILE_MUTATION_TIMEOUT_MS } ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const summary = fileOwnershipWorktreeRead.interpret(worktreeReply) as - | { hostId?: string | null } - | undefined + const summary = fileOwnershipWorktreeRead.interpret(worktreeReply) if (!summary) { throw new Error(SSH_OWNER_CHANGED_MESSAGE) } const host = parseExecutionHostId(summary.hostId) - let sshState: SshConnectionState | null = null + let sshState: MobileFileMutationSshState = null if (host?.kind === 'ssh') { const stateReply = await fileOwnershipSshStateRead.request( client, { targetId: host.targetId }, { timeoutMs: FILE_MUTATION_TIMEOUT_MS } ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - sshState = fileOwnershipSshStateRead.interpret(stateReply) as SshConnectionState | null + sshState = fileOwnershipSshStateRead.interpret(stateReply) } return buildMobileFileMutationOwnership(summary.hostId, sshState) } diff --git a/mobile/src/files/mobile-file-ownership-operations.ts b/mobile/src/files/mobile-file-ownership-operations.ts index 82084993729..520cccba42f 100644 --- a/mobile/src/files/mobile-file-ownership-operations.ts +++ b/mobile/src/files/mobile-file-ownership-operations.ts @@ -1,9 +1,15 @@ import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' -import { rpcUncheckedMemberReader } from '../transport/rpc-reader-payload' +import { rpcResultVariant } from '../transport/rpc-operation-result-reader' +import { + fileOwnershipSshStateSchema, + fileOwnershipWorktreeSchema +} from './file-ownership-reply-schema' // The three reads that pin which execution host owns a workspace before a file mutation is sent. // All three share one acceptance because the capture is all-or-nothing: any refusal aborts the -// mutation with the host's own message rather than letting a write land on the wrong host. +// mutation with the host's own message rather than letting a write land on the wrong host. An +// unreadable reply now aborts it the same way, with the method named, where main read a member off +// the cast payload and either threw a raw TypeError or captured an owner it had not checked. // The runtime status this gate needs is the one the Tasks screen already asks for, field for // field. A second operation would only be a second name for the same wire. @@ -16,7 +22,7 @@ export const fileOwnershipWorktreeRead = bindDeferredRpcOperation( method: 'worktree.show', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedMemberReader('worktree-summary', 'worktree') + read: rpcResultVariant('worktree-summary', fileOwnershipWorktreeSchema) }) ) @@ -27,7 +33,7 @@ export const fileOwnershipSshStateRead = bindDeferredRpcOperation( method: 'ssh.getState', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedMemberReader('ssh-connection-state', 'state') + read: rpcResultVariant('ssh-connection-state', fileOwnershipSshStateSchema) }) ) diff --git a/mobile/src/files/mobile-file-preview-operations.ts b/mobile/src/files/mobile-file-preview-operations.ts index ac679e61bce..eeb3c1fcb8c 100644 --- a/mobile/src/files/mobile-file-preview-operations.ts +++ b/mobile/src/files/mobile-file-preview-operations.ts @@ -1,5 +1,11 @@ import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' -import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' +import { rpcResultVariant } from '../transport/rpc-operation-result-reader' +import { + filePreviewImageSchema, + filePreviewTextSchema, + terminalArtifactWriteSchema, + terminalPathResolutionSchema +} from './file-preview-reply-schema' /** * The preview screen's reads and writes. @@ -10,9 +16,16 @@ import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' * the failure is a stale terminal-artifact grant worth refreshing. No acceptance policy exposes a * refusal code, and only these two consumers want one. * - * The payloads are unchecked here because the shape depends on the path, not on the method: - * `normalizeMobileFilePreviewResult` picks the image or text projection from the file name, which - * a module-level reader cannot see. + * What a *malformed* accepted reply does is what changes here. A skip's reader is consulted only + * after the policy has already admitted the reply, so an unreadable payload throws + * `RpcIncompatibleReplyError` naming the method; the preview screen's own try/catch runs it back + * through `previewError`, which lands on 'Unable to load preview' — the copy main already showed + * for an unreadable text payload, and a truer one than the 'Binary preview unavailable' main gave + * an unreadable image payload. + * + * The text and image readers are split by method rather than by path: `createMobileFilePreviewRequest` + * picks the method from `classifyMobileArtifact`, and `loadMobileFilePreview` normalizes with the + * same predicate over the same path, so each method has exactly one projection behind it. */ /** files.read for a preview. The tab doc asks the same method under a throwing policy. */ @@ -22,7 +35,7 @@ export const filePreviewTextRead = bindDeferredRpcOperation( method: 'files.read', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('file-preview') + read: rpcResultVariant('file-preview-text', filePreviewTextSchema) }) ) @@ -33,7 +46,7 @@ export const filePreviewImageRead = bindDeferredRpcOperation( method: 'files.readPreview', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('file-preview') + read: rpcResultVariant('file-preview-image', filePreviewImageSchema) }) ) @@ -43,7 +56,7 @@ export const terminalArtifactTextRead = bindDeferredRpcOperation( method: 'files.readTerminalArtifact', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('file-preview') + read: rpcResultVariant('file-preview-text', filePreviewTextSchema) }) ) @@ -53,7 +66,7 @@ export const terminalArtifactImageRead = bindDeferredRpcOperation( method: 'files.readTerminalArtifactPreview', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('file-preview') + read: rpcResultVariant('file-preview-image', filePreviewImageSchema) }) ) @@ -64,7 +77,7 @@ export const terminalArtifactWrite = bindDeferredRpcOperation( method: 'files.writeTerminalArtifact', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('artifact-written') + read: rpcResultVariant('artifact-written', terminalArtifactWriteSchema) }) ) @@ -75,7 +88,7 @@ export const terminalArtifactPathResolve = bindDeferredRpcOperation( method: 'files.resolveTerminalPath', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('terminal-path-resolution') + read: rpcResultVariant('terminal-path-resolution', terminalPathResolutionSchema) }) ) diff --git a/mobile/src/files/mobile-file-preview-request.test.ts b/mobile/src/files/mobile-file-preview-request.test.ts index 9c8692e5a78..ff1faa168ef 100644 --- a/mobile/src/files/mobile-file-preview-request.test.ts +++ b/mobile/src/files/mobile-file-preview-request.test.ts @@ -10,6 +10,7 @@ import { normalizeMobileFilePreviewResult, previewErrorFromRefusal } from './mobile-file-preview-response' +import { RPC_INCOMPATIBLE_REPLY_CODE } from '../transport/rpc-incompatible-reply-error' function ok(result: unknown): RpcSuccess { return { id: '1', ok: true, result, _meta: { runtimeId: 'runtime-1' } } @@ -501,7 +502,7 @@ describe('mobile-file-preview-request', () => { }) }) - it('reports a malformed refreshed artifact read instead of treating it as changed desktop content', async () => { + it('names the unreadable reply on a refreshed artifact read instead of treating it as changed desktop content', async () => { const client = clientWithResponses([ fail('terminal_file_grant_stale'), ok({ @@ -532,10 +533,9 @@ describe('mobile-file-preview-request', () => { '{"ok":false}', { baseContent: '{"ok":true}' } ) - ).resolves.toEqual({ - status: 'error', - message: 'Unable to load preview', - reconnect: false + ).rejects.toMatchObject({ + code: RPC_INCOMPATIBLE_REPLY_CODE, + method: 'files.readTerminalArtifact' }) expect(client.sendRequest).toHaveBeenCalledTimes(3) diff --git a/mobile/src/files/mobile-file-tab-doc-operations.ts b/mobile/src/files/mobile-file-tab-doc-operations.ts index 244d44e5e39..e14e5cecfc4 100644 --- a/mobile/src/files/mobile-file-tab-doc-operations.ts +++ b/mobile/src/files/mobile-file-tab-doc-operations.ts @@ -1,5 +1,12 @@ import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' -import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' +import { rpcResultVariant, rpcResultVariants } from '../transport/rpc-operation-result-reader' +import { + fileTabBinaryDiffSchema, + fileTabImageSchema, + fileTabTextDiffSchema, + fileTabTextSchema, + type MobileFileTabDiff +} from './file-tab-doc-reply-schema' /** * What a session file tab reads to render one document. @@ -9,17 +16,30 @@ import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' * while the preview screen renders the refusal as body copy. Two policies, two families, named * here and in mobile-file-preview-operations.ts so neither can drift onto the other. * - * The payloads stay unchecked: the tab picks its projection from the path, and moving a shape - * check into a reader would reject replies the tab renders today. + * The readers are stricter than the preview screen's for the same reason the policies differ: a tab + * publishes what it read into a typed ready document with no guard, so a member the preview screen + * normalizes is one the tab renders as `undefined`. An unreadable reply now reaches `readFileTab`'s + * catch as one named error instead of a property-read TypeError, and that catch already shows + * "Couldn't load file preview" for both. */ +/** + * The diff a staged or unstaged tab renders. + * + * Two variants, because the host's own result is a union whose arms require different members and + * whose arm set is a wire surface: a `kind` this build has not heard of takes the binary arm, which + * is the branch main's `kind !== 'text'` already sent it down. + */ export const fileTabDiffRead = bindDeferredRpcOperation( defineRpcOperation({ name: 'git.file-tab-diff', method: 'git.diff', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('file-tab-diff') + read: rpcResultVariants<'file-tab-text-diff' | 'file-tab-binary-diff', MobileFileTabDiff>([ + rpcResultVariant('file-tab-text-diff', fileTabTextDiffSchema), + rpcResultVariant('file-tab-binary-diff', fileTabBinaryDiffSchema) + ]) }) ) @@ -29,7 +49,7 @@ export const fileTabTextRead = bindDeferredRpcOperation( method: 'files.read', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('file-tab-text') + read: rpcResultVariant('file-tab-text', fileTabTextSchema) }) ) @@ -39,7 +59,7 @@ export const fileTabImageRead = bindDeferredRpcOperation( method: 'files.readPreview', acceptance: 'require-result-or-throw-message', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('file-tab-image') + read: rpcResultVariant('file-tab-image', fileTabImageSchema) }) ) diff --git a/mobile/src/files/mobile-file-tab-doc.ts b/mobile/src/files/mobile-file-tab-doc.ts index 1e9d43abbba..b22ae9fe396 100644 --- a/mobile/src/files/mobile-file-tab-doc.ts +++ b/mobile/src/files/mobile-file-tab-doc.ts @@ -1,7 +1,7 @@ import { buildImageDataUri } from '../../../src/shared/image-data-uri' import { classifyMobileArtifact } from '../session/mobile-artifact-kind' import { buildMobileDiffLines, type MobileDiffLine } from '../session/mobile-diff-lines' -import { mobileDiffImageDataUri, type MobileBinaryDiffResult } from './mobile-diff-image-preview' +import { mobileDiffImageDataUri } from './mobile-diff-image-preview' import { fileTabDiffRead, fileTabImageRead, @@ -37,10 +37,7 @@ export async function resolveMobileFileTabDoc( filePath: relativePath, staged: request.diffSource === 'staged' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const result = fileTabDiffRead.interpret(reply) as - | { kind: 'text'; originalContent: string; modifiedContent: string } - | MobileBinaryDiffResult + const result = fileTabDiffRead.interpret(reply) if (result.kind !== 'text') { // Render image diffs (add/modify/delete) from the base64 the host already // sends; only non-previewable binaries stay unavailable. @@ -57,12 +54,7 @@ export async function resolveMobileFileTabDoc( const artifactKind = classifyMobileArtifact(relativePath) if (artifactKind === 'image') { const preview = await fileTabImageRead.request(client, { worktree, relativePath }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const result = fileTabImageRead.interpret(preview) as { - content: string - isImage?: boolean - mimeType?: string - } + const result = fileTabImageRead.interpret(preview) const dataUri = result.isImage ? buildImageDataUri(result.mimeType, result.content) : null if (!dataUri) { throw new Error('binary_file') @@ -71,12 +63,7 @@ export async function resolveMobileFileTabDoc( } const reply = await fileTabTextRead.request(client, { worktree, relativePath }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const result = fileTabTextRead.interpret(reply) as { - content: string - truncated: boolean - byteLength: number - } + const result = fileTabTextRead.interpret(reply) if (artifactKind === 'html') { return { status: 'ready', kind: 'html', content: result.content } } diff --git a/mobile/src/hooks/use-now.test.ts b/mobile/src/hooks/use-now.test.ts index 14264b513ee..9248cac492f 100644 --- a/mobile/src/hooks/use-now.test.ts +++ b/mobile/src/hooks/use-now.test.ts @@ -59,23 +59,31 @@ describe('useNow', () => { it('ticks while active, pauses in the background, and refreshes immediately on resume', () => { expect(latest).toBe(1_000) - act(() => vi.advanceTimersByTime(1_000)) + act(() => { + vi.advanceTimersByTime(1_000) + }) expect(latest).toBe(2_000) changeAppState('background') - act(() => vi.advanceTimersByTime(5_000)) + act(() => { + vi.advanceTimersByTime(5_000) + }) expect(latest).toBe(2_000) changeAppState('active') expect(latest).toBe(7_000) - act(() => vi.advanceTimersByTime(1_000)) + act(() => { + vi.advanceTimersByTime(1_000) + }) expect(latest).toBe(8_000) }) it('stops while disabled and refreshes immediately when re-enabled', () => { act(() => renderer?.update(createElement(Harness, { enabled: false }))) - act(() => vi.advanceTimersByTime(5_000)) + act(() => { + vi.advanceTimersByTime(5_000) + }) expect(latest).toBe(1_000) act(() => renderer?.update(createElement(Harness, { enabled: true }))) diff --git a/mobile/src/host-screen/host-screen-operations.ts b/mobile/src/host-screen/host-screen-operations.ts index d0ddc342ac8..fa374ce6bc4 100644 --- a/mobile/src/host-screen/host-screen-operations.ts +++ b/mobile/src/host-screen/host-screen-operations.ts @@ -1,12 +1,21 @@ import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { rpcResultVariant } from '../transport/rpc-operation-result-reader' import { - rpcUncheckedMemberReader, - rpcUncheckedPayloadReader -} from '../transport/rpc-reader-payload' + hostPlatformSchema, + hostRepoCatalogSchema, + hostScreenUnreadReplySchema, + hostSshTargetSummariesSchema, + hostViewSettingsSchema +} from './host-screen-reply-schema' // What the host screen reads to label its rows and to mirror the desktop's workspace view store. // Every read here is decorative: a refusal leaves the screen on what it already has and the next // refresh retries, so all of them skip rather than throw. +// +// A skip's reader runs only on a reply the policy already admitted, so an unreadable one throws +// rather than skipping. Both readers that project a list are inside the metadata refresh's own +// try/catch, which already treats a failed refresh as "retry on the next one"; the four writes read +// no reply body at all. host-screen-reply-schema.ts says which members each screen actually reads. export const hostRepoCatalogRead = bindDeferredRpcOperation( defineRpcOperation({ @@ -14,7 +23,7 @@ export const hostRepoCatalogRead = bindDeferredRpcOperation( method: 'repo.list', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('repo-catalog') + read: rpcResultVariant('repo-catalog', hostRepoCatalogSchema) }) ) @@ -25,7 +34,7 @@ export const hostSshTargetSummariesRead = bindDeferredRpcOperation( method: 'ssh.listTargetSummaries', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('ssh-target-summaries') + read: rpcResultVariant('ssh-target-summaries', hostSshTargetSummariesSchema) }) ) @@ -35,7 +44,7 @@ export const hostPlatformRead = bindDeferredRpcOperation( method: 'host.platform', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('host-platform') + read: rpcResultVariant('host-platform', hostPlatformSchema) }) ) @@ -51,7 +60,7 @@ export const hostViewSettingsRead = bindDeferredRpcOperation( method: 'ui.get', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedMemberReader('ui-view-settings', 'ui') + read: rpcResultVariant('ui-view-settings', hostViewSettingsSchema) }) ) @@ -62,7 +71,7 @@ export const hostViewSettingsWrite = bindDeferredRpcOperation( method: 'ui.set', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('ui-view-settings-written') + read: rpcResultVariant('ui-view-settings-written', hostScreenUnreadReplySchema) }) ) @@ -79,7 +88,7 @@ export const worktreePinWrite = bindDeferredRpcOperation( method: 'worktree.set', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('pin-written') + read: rpcResultVariant('pin-written', hostScreenUnreadReplySchema) }) ) @@ -90,7 +99,7 @@ export const worktreeRemove = bindDeferredRpcOperation( method: 'worktree.rm', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('worktree-removed') + read: rpcResultVariant('worktree-removed', hostScreenUnreadReplySchema) }) ) @@ -108,6 +117,6 @@ export const worktreeActivate = bindDeferredRpcOperation( method: 'worktree.activate', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: rpcUncheckedPayloadReader('worktree-activated') + read: rpcResultVariant('worktree-activated', hostScreenUnreadReplySchema) }) ) diff --git a/mobile/src/host-screen/host-screen-reply-schema.test.ts b/mobile/src/host-screen/host-screen-reply-schema.test.ts new file mode 100644 index 00000000000..0f36f86e3d4 --- /dev/null +++ b/mobile/src/host-screen/host-screen-reply-schema.test.ts @@ -0,0 +1,175 @@ +import { describe, expect, it } from 'vitest' +import type { RepoIcon } from '../../../src/shared/repo-icon' +import { NODE_PLATFORM_NAMES } from '../transport/mobile-runtime-host-platform' +import { + hostPlatformSchema, + hostRepoCatalogSchema, + hostSshTargetSummariesSchema, + hostViewSettingsSchema, + WORKSPACE_GROUP_BY_ARMS, + WORKSPACE_SORT_BY_ARMS +} from './host-screen-reply-schema' + +describe('host screen reply schemas', () => { + it('requires the repo list and drops a row no map could key', () => { + expect(hostRepoCatalogSchema.safeParse({}).success).toBe(false) + const parsed = hostRepoCatalogSchema.parse({ + repos: [{ id: 'r1', displayName: 'orca' }, { id: 'r2' }, { displayName: 'ghost' }] + }) + expect(parsed.map((repo) => repo.id)).toEqual(['r1']) + }) + + it('salvages badgeColor onto the generated swatch main fell back to', () => { + expect( + hostRepoCatalogSchema.parse({ repos: [{ id: 'r', displayName: 'o', badgeColor: 3 }] })[0] + ?.badgeColor + ).toBeUndefined() + }) + + it('keeps a known repo icon arm and degrades an unknown one to absent', () => { + const known = hostRepoCatalogSchema.parse({ + repos: [{ id: 'r', displayName: 'o', repoIcon: { type: 'emoji', emoji: 'x' } }] + }) + expect(known[0]?.repoIcon).toEqual({ type: 'emoji', emoji: 'x' }) + const unknown = hostRepoCatalogSchema.parse({ + repos: [{ id: 'r', displayName: 'o', repoIcon: { type: 'svg', markup: '' } }] + }) + // The row survives and draws the Folder default MobileRepoIcon already drew for an arm it + // could not match. + expect(unknown[0]?.id).toBe('r') + expect(unknown[0]?.repoIcon).toBeUndefined() + }) + + it('keeps an image icon whose source this build has never heard of', () => { + const [repo] = hostRepoCatalogSchema.parse({ + repos: [ + { + id: 'r', + displayName: 'o', + repoIcon: { + type: 'image', + src: 'https://example.invalid/a.png', + source: 'gitlab', + label: 'acme/orca' + } + } + ] + }) + // MobileRepoIcon reads src and label and never source, so narrowing source would have drawn a + // Folder where main drew the image. The member itself still reaches the row. + expect(repo?.repoIcon).toEqual({ + type: 'image', + src: 'https://example.invalid/a.png', + source: 'gitlab', + label: 'acme/orca' + }) + }) + + it('passes a host-id spelling through for getRepoExecutionHostId to judge', () => { + const [repo] = hostRepoCatalogSchema.parse({ + repos: [{ id: 'r', displayName: 'o', executionHostId: 'cloud:zone-a', connectionId: null }] + }) + expect(repo?.executionHostId).toBe('cloud:zone-a') + expect(repo?.connectionId).toBeNull() + }) + + it('degrades an unreadable ssh target list to the empty one that falls back to host ids', () => { + expect(hostSshTargetSummariesSchema.parse({})).toEqual([]) + expect(hostSshTargetSummariesSchema.parse({ targets: 'none' })).toEqual([]) + expect( + hostSshTargetSummariesSchema.parse({ + targets: [{ id: 't', label: 'T' }, { id: 'u' }, { label: 'V' }] + }) + ).toEqual([{ id: 't', label: 'T' }]) + }) + + it('answers the empty target list for a reply that is no object at all', () => { + // Why: the label write runs before the platform write in the same sequence, so a throw here + // would take the platform down with it. readSshTargets answered [] for every one of these. + for (const payload of [null, undefined, 'nope', 42, []]) { + expect(hostSshTargetSummariesSchema.parse(payload)).toEqual([]) + } + }) + + it('reads only a platform Node could have reported', () => { + expect(hostPlatformSchema.parse({ platform: 'win32' })).toBe('win32') + expect(hostPlatformSchema.parse({ platform: 'plan9' })).toBeNull() + expect(hostPlatformSchema.parse({ platform: '' })).toBeNull() + expect(hostPlatformSchema.parse({})).toBeNull() + for (const payload of [null, undefined, 'nope', 42]) { + expect(hostPlatformSchema.parse(payload)).toBeNull() + } + }) + + it('requires the ui member main read with a bare property access', () => { + expect(hostViewSettingsSchema.safeParse({}).success).toBe(false) + expect(hostViewSettingsSchema.safeParse(null).success).toBe(false) + expect(hostViewSettingsSchema.parse({ ui: {} })).toEqual({}) + }) + + it('degrades an unknown grouping or sort arm to absent so the local mode is kept', () => { + const parsed = hostViewSettingsSchema.parse({ + ui: { groupBy: 'agent', sortBy: 'stars', hideSleepingWorkspaces: 'yes' } + }) + expect(parsed.groupBy).toBeUndefined() + expect(parsed.sortBy).toBeUndefined() + expect(parsed.hideSleepingWorkspaces).toBeUndefined() + }) + + it('keeps the known view arms and the lists the screen adopts', () => { + const parsed = hostViewSettingsSchema.parse({ + ui: { + groupBy: 'workspace-status', + sortBy: 'recent', + filterRepoIds: ['r1'], + collapsedGroups: [], + workspaceStatuses: [{ id: 'active', label: 'Active' }] + } + }) + expect(parsed.groupBy).toBe('workspace-status') + expect(parsed.sortBy).toBe('recent') + expect(parsed.filterRepoIds).toEqual(['r1']) + expect(parsed.workspaceStatuses).toEqual([{ id: 'active', label: 'Active' }]) + }) + + it('salvages a non-array status catalog rather than handing a string to the group lookups', () => { + expect( + hostViewSettingsSchema.parse({ ui: { workspaceStatuses: 'active' } }).workspaceStatuses + ).toBeUndefined() + }) +}) + +describe('the closed arm sets are the desktop unions', () => { + // The arm lists are pinned to the desktop unions in the schema modules, where tsc looks; these + // loops prove every pinned arm survives the parse, not just the ones the tests above pick. + it('keeps every platform Node can report', () => { + for (const platform of NODE_PLATFORM_NAMES) { + expect(hostPlatformSchema.parse({ platform })).toBe(platform) + } + }) + + it('keeps every grouping and sort arm the desktop persists', () => { + for (const groupBy of WORKSPACE_GROUP_BY_ARMS) { + for (const sortBy of WORKSPACE_SORT_BY_ARMS) { + expect(hostViewSettingsSchema.parse({ ui: { groupBy, sortBy } })).toMatchObject({ + groupBy, + sortBy + }) + } + } + }) + + it('keeps every repo icon arm the desktop draws', () => { + const icons: Record> = { + lucide: { type: 'lucide', name: 'Folder' }, + emoji: { type: 'emoji', emoji: '🐳' }, + image: { type: 'image', src: 'data:,x' } + } + for (const repoIcon of Object.values(icons)) { + const [row] = hostRepoCatalogSchema.parse({ + repos: [{ id: 'r', displayName: 'r', repoIcon }] + }) + expect(row?.repoIcon).toMatchObject({ type: repoIcon.type }) + } + }) +}) diff --git a/mobile/src/host-screen/host-screen-reply-schema.ts b/mobile/src/host-screen/host-screen-reply-schema.ts new file mode 100644 index 00000000000..ddc72e29966 --- /dev/null +++ b/mobile/src/host-screen/host-screen-reply-schema.ts @@ -0,0 +1,181 @@ +import { z } from 'zod' +import type { PersistedUIState } from '../../../src/shared/persisted-ui-state-types' +import type { RepoIcon } from '../../../src/shared/repo-icon' +import { hostUnionArms, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage' +import { NODE_PLATFORM_NAMES } from '../transport/mobile-runtime-host-platform' + +// The closed arm sets on this screen are the desktop's own unions, pinned through hostUnionArms so +// an arm the desktop adds or drops fails tsc here rather than degrading silently on the phone. +export const WORKSPACE_GROUP_BY_ARMS = hostUnionArms({ + none: true, + 'workspace-status': true, + repo: true, + 'pr-status': true +}) +export const WORKSPACE_SORT_BY_ARMS = hostUnionArms({ + name: true, + smart: true, + recent: true, + repo: true, + manual: true +}) + +// One branch per RepoIcon arm; `satisfies` over the mapped union fails tsc on a missing or stale arm. +const repoIconBranches = { + lucide: z.looseObject({ type: z.literal('lucide'), name: z.string() }), + emoji: z.looseObject({ type: z.literal('emoji'), emoji: z.string() }), + image: z.looseObject({ + type: z.literal('image'), + src: z.string(), + label: salvagedOptional('label', z.string()) + }) +} satisfies Readonly> + +// What the host screen reads to label its rows and to mirror the desktop's workspace view store. +// Checked against src/main/runtime/rpc/methods/repo.ts:29, ssh.ts:55, host-capabilities.ts:8 and +// client-ui.ts:60/65, and the shared records they return: Repo in src/shared/repo-types.ts:42 and +// PersistedUIState's workspace-view subset in mobile/src/worktree/workspace-view-settings.ts:14. + +/** + * The host's repo catalog, narrowed to what the label maps are built from. + * + * `id` and `displayName` are required and a row without either drops: `displayName` is the key of + * four Maps and the argument `repoColor` hashes with `name.charCodeAt` — a row without one was a + * TypeError that took the whole metadata refresh with it — and `id` is the Map value the workspace + * rows resolve their host through. + * + * `badgeColor` sits behind main's own `||` fallback to `repoColor`, so a salvaged member draws the + * same swatch. `repoIcon` is declared as the three arms MobileRepoIcon renders, and an arm this + * build has not heard of degrades to absent — which is the Folder default that component already + * drew for an arm it could not match, so the row keeps its label either way. + * The image arm deliberately stops at `src` and `label`: those are the members the component reads, + * and `source` — which it never reads — is left to pass through. Declaring it as the four arms + * `RepoIconImageSource` spells today would have dropped the WHOLE icon for a source a later host + * adds, drawing a Folder where main drew the image; passthrough keeps the member on the object + * verbatim, which is also what `settings-repo-metadata-icons` records. + * `connectionId` and `executionHostId` are declared as plain strings rather than as + * the host-id template union they are typed with: the union is a wire surface, and + * `getRepoExecutionHostId` — which is what every read of them goes through — already answers `local` + * for a spelling it cannot parse. Narrowing them here would refuse a newer host's own rows. + */ +export const hostRepoCatalogSchema = z + .looseObject({ + repos: salvagingArray( + z.looseObject({ + id: z.string(), + displayName: z.string(), + badgeColor: salvagedOptional('badgeColor', z.string()), + repoIcon: salvagedOptional( + 'repoIcon', + z.union([repoIconBranches.lucide, repoIconBranches.emoji, repoIconBranches.image]) + ), + connectionId: salvagedOptional('connectionId', z.string().nullable()), + executionHostId: salvagedOptional('executionHostId', z.string().nullable()) + }) + ) + }) + .transform((reply) => reply.repos) + +/** + * The SSH target labels a mixed-host catalog names its rows with. + * + * The rows the label builder keeps are exactly the rows with a string `id` and `label`, so the + * filter that used to sit in `readSshTargets` is the schema now and a row without either drops. + * `targets` itself is salvaged rather than required because main answered `[]` for a reply without + * it, and a `[]` here is what makes the labels degrade to host ids — the documented behaviour for a + * host that predates the method. + * + * Total, like the reader it replaces: `readSshTargets` answered `[]` for any payload at all, and + * the caller writes the labels before it reads the platform, so a throw here would also skip the + * platform write. The `.catch` keeps a non-object reply degrading exactly where main degraded. + */ +export const hostSshTargetSummariesSchema = z + .looseObject({ + targets: salvagedOptional( + 'targets', + salvagingArray(z.looseObject({ id: z.string(), label: z.string() })) + ) + }) + .transform((reply) => reply.targets ?? []) + .catch(() => []) + +/** + * The paired host's own platform. + * + * Salvaged to absent, which reads as null — main's own answer for a non-string or an empty one + * (`typeof platform === 'string' && platform`), and the value that keeps the phone's platform from + * naming the desktop. The arm set is closed over Node's platform domain rather than over anything + * Orca versions: the handler returns `process.platform` and nothing else, and a string outside that + * set names no path convention this client could apply. + * + * Total for the same reason as the SSH targets above: `readHostPlatform` answered `null` for any + * payload, so a non-object reply degrades here instead of throwing past the label write. + */ +export const hostPlatformSchema = z + .looseObject({ platform: salvagedOptional('platform', z.enum(NODE_PLATFORM_NAMES)) }) + .transform((reply) => reply.platform ?? null) + .catch(() => null) + +/** + * The desktop's shared workspace view settings, read off `ui.get`'s `ui` member. + * + * Nothing is required: applyDesktopViewSettings reads every member behind `??` or a mapping table + * that answers null for an arm it does not know, so a salvaged member leaves the local value in + * place — which is exactly what main did for an absent one. `groupBy` and `sortBy` are closed arm + * sets that degrade to absent for the same reason: every read is a lookup that already fell back to + * the current mode for an arm it could not map, so nothing is withheld that the reply granted. + * + * `workspaceStatuses` keeps its rows opaque — coerceMobileWorkspaceStatuses only counts them — but + * the container is checked, because main handed a non-array straight into the status catalog and + * every group lookup then read `.find` off a string. + * + * The `ui` member itself is required. Main read it off the payload with a bare property access that + * threw a TypeError on a null result, and the host screen's own try/catch is where that throw has + * always landed; an incompatible reply reaches the same catch with the method named. + */ +export const hostViewSettingsSchema = z + .looseObject({ + ui: z.looseObject({ + groupBy: salvagedOptional('groupBy', z.enum(WORKSPACE_GROUP_BY_ARMS)), + sortBy: salvagedOptional('sortBy', z.enum(WORKSPACE_SORT_BY_ARMS)), + hideSleepingWorkspaces: salvagedOptional('hideSleepingWorkspaces', z.boolean()), + hideDefaultBranchWorkspace: salvagedOptional('hideDefaultBranchWorkspace', z.boolean()), + alwaysShowDefaultBranchWorkspace: salvagedOptional( + 'alwaysShowDefaultBranchWorkspace', + z.boolean() + ), + filterRepoIds: salvagedOptional('filterRepoIds', z.array(z.string())), + collapsedGroups: salvagedOptional('collapsedGroups', z.array(z.string())), + workspaceStatuses: salvagedOptional( + 'workspaceStatuses', + z.array(z.looseObject({ id: z.string(), label: z.string() })) + ) + }) + }) + .transform((reply) => reply.ui) + +/** + * The four host-list writes whose reply body no call site reads. + * + * The `ui.set` patch, the pin write, the row delete and the activate ping are all decided by the + * acceptance verdict alone — the pin write never interprets its reply at all, and the delete reads + * `accepted` and nothing else. + * + * `worktree.activate` is the one of the four whose payload a *second* consumer looks at, and it is + * deliberately left opaque: headlessActivationNeedsHostRenderer is a total guard over `unknown` + * (worktree-activation-result.ts:1), and the session route's second report site + * (use-mobile-session-startup.ts:170) reports from inside a fire-and-forget `void (async …)()` with + * no catch of its own, so a reader that could throw would turn an unreadable activation into an + * unhandled rejection *and* skip the terminal fetch below it, where main showed no toast and + * fetched. This schema staying total is what holds that site safe; the first report site + * (:141) is chained `.then(…).catch(…)` and would survive a throw. + */ +export const hostScreenUnreadReplySchema = z.unknown() + +/** One decoded catalog icon: the members MobileRepoIcon reads, with the rest passed through. */ +export type MobileHostRepoIcon = NonNullable< + z.output[number]['repoIcon'] +> + +/** What MobileRepoIcon renders: a decoded catalog icon, or the `RepoIcon` a worktree row carries. */ +export type MobileRenderableRepoIcon = MobileHostRepoIcon | RepoIcon diff --git a/mobile/src/host-screen/use-host-repo-metadata.ts b/mobile/src/host-screen/use-host-repo-metadata.ts index d87d917b467..0987aed14b0 100644 --- a/mobile/src/host-screen/use-host-repo-metadata.ts +++ b/mobile/src/host-screen/use-host-repo-metadata.ts @@ -5,7 +5,6 @@ import { setCachedRepos } from '../cache/repo-cache' import type { RpcAcceptedResult } from '../transport/rpc-accepted-result' import type { RpcClient } from '../transport/rpc-client' import type { ConnectionState, RpcResponse } from '../transport/types' -import type { RepoSummary } from '../worktree/host-worktree-rpc-types' import { repoColor } from '../worktree/repo-color' import { buildHostLabelById, @@ -20,8 +19,6 @@ import type { HostScreenState } from './use-host-screen-state' const REPO_METADATA_REFRESH_MS = 60_000 -type SshTargetSummaryRow = { id: string; label: string } - async function settledMetadataReply(send: () => Promise): Promise { try { return await send() @@ -32,10 +29,10 @@ async function settledMetadataReply(send: () => Promise): Promise( reply: RpcResponse | null, - interpret: (reply: RpcResponse) => RpcAcceptedResult -): unknown { + interpret: (reply: RpcResponse) => RpcAcceptedResult +): Value | null { if (!reply) { return null } @@ -43,25 +40,6 @@ function acceptedMetadata( return verdict.accepted ? verdict.value : null } -function readSshTargets(result: unknown): SshTargetSummaryRow[] { - const targets = (result as { targets?: unknown } | null)?.targets - if (!Array.isArray(targets)) { - return [] - } - return targets.filter( - (target): target is SshTargetSummaryRow => - typeof target === 'object' && - target !== null && - typeof (target as SshTargetSummaryRow).id === 'string' && - typeof (target as SshTargetSummaryRow).label === 'string' - ) -} - -function readHostPlatform(result: unknown): NodeJS.Platform | null { - const platform = (result as { platform?: unknown } | null)?.platform - return typeof platform === 'string' && platform ? (platform as NodeJS.Platform) : null -} - function readHostSettingOverrides(result: unknown): unknown { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. return (result as { hostSettingOverrides?: unknown } | null)?.hostSettingOverrides @@ -118,13 +96,12 @@ export function useHostRepoMetadata(args: { if (!repos || !repos.accepted) { return } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const repoResult = repos.value as { repos: RepoSummary[] } + const catalog = repos.value repoMetadataFetchedAtRef.current = Date.now() - setCachedRepos(requestHostId, repoResult.repos) + setCachedRepos(requestHostId, catalog) setRepoColorsByName( new Map( - repoResult.repos.map((repo) => [ + catalog.map((repo) => [ repo.displayName, repo.badgeColor || repoColor(repo.displayName) ]) @@ -132,17 +109,17 @@ export function useHostRepoMetadata(args: { ) setRepoIconsByName( new Map( - repoResult.repos.flatMap((repo) => + catalog.flatMap((repo) => repo.repoIcon ? [[repo.displayName, repo.repoIcon] as const] : [] ) ) ) - setRepoIdsByName(new Map(repoResult.repos.map((repo) => [repo.displayName, repo.id]))) - setRepoHostIdByRepoId(buildRepoHostIdByRepoId(repoResult.repos)) + setRepoIdsByName(new Map(catalog.map((repo) => [repo.displayName, repo.id]))) + setRepoHostIdByRepoId(buildRepoHostIdByRepoId(catalog)) // Why: rows only name their host when the list spans hosts, so a single-host // catalog never pays for the label lookups. Counted over repos, not the id-keyed // map: one repo id registered on two hosts is two hosts. - const hostIds = new Set(repoResult.repos.map((repo) => getRepoExecutionHostId(repo))) + const hostIds = new Set(catalog.map((repo) => getRepoExecutionHostId(repo))) if (hostIds.size > 1) { const [sshTargets, hostSettings, hostPlatform] = await Promise.all([ settledMetadataReply(() => hostSshTargetSummariesRead.request(requestClient)), @@ -157,17 +134,14 @@ export function useHostRepoMetadata(args: { : null setHostLabelById( buildHostLabelById({ - sshTargets: readSshTargets( - acceptedMetadata(sshTargets, hostSshTargetSummariesRead.interpret) - ), + sshTargets: + acceptedMetadata(sshTargets, hostSshTargetSummariesRead.interpret) ?? [], hostSettingOverrides: readHostSettingOverrides( hostSettingsResult?.accepted ? hostSettingsResult.value : undefined ) }) ) - setHostPlatform( - readHostPlatform(acceptedMetadata(hostPlatform, hostPlatformRead.interpret)) - ) + setHostPlatform(acceptedMetadata(hostPlatform, hostPlatformRead.interpret) ?? null) } } while (fetchRepoMetadataPendingRef.current.has(requestClient)) } catch { diff --git a/mobile/src/host-screen/use-host-screen-state.ts b/mobile/src/host-screen/use-host-screen-state.ts index ca6bd0d85e7..9f6f8358c6f 100644 --- a/mobile/src/host-screen/use-host-screen-state.ts +++ b/mobile/src/host-screen/use-host-screen-state.ts @@ -1,6 +1,5 @@ import { useRef, useState } from 'react' import type { ExecutionHostId } from '../../../src/shared/execution-host' -import type { RepoIcon } from '../../../src/shared/repo-icon' import type { WorkspaceStatusDefinition } from '../../../src/shared/worktree/types' import { getCachedWorktrees } from '../cache/worktree-cache' import { createInitialHostRouteActionState } from '../host-route-action-state' @@ -13,6 +12,7 @@ import type { MobileViewState } from '../worktree/workspace-view-settings' import type { FilterState, Worktree } from '../worktree/workspace-list-sections' +import type { MobileHostRepoIcon } from './host-screen-reply-schema' export function useHostScreenState(hostId: string | undefined, action: string | undefined) { const [initialCache] = useState(() => @@ -38,7 +38,7 @@ export function useHostScreenState(hostId: string | undefined, action: string | string | null >(null) const [repoColorsByName, setRepoColorsByName] = useState>(new Map()) - const [repoIconsByName, setRepoIconsByName] = useState>(new Map()) + const [repoIconsByName, setRepoIconsByName] = useState>(new Map()) const [hostName, setHostName] = useState('') const [error, setError] = useState('') const [lastKnownWorktrees, setLastKnownWorktrees] = useState(initialCache ?? []) diff --git a/mobile/src/notifications/NotificationDeliverySection.test.tsx b/mobile/src/notifications/NotificationDeliverySection.test.tsx index 8b7971b6086..f51614a821f 100644 --- a/mobile/src/notifications/NotificationDeliverySection.test.tsx +++ b/mobile/src/notifications/NotificationDeliverySection.test.tsx @@ -14,7 +14,7 @@ vi.mock('react-native', () => ({ it('shows only phone-specific controls while desktop owns category eligibility', () => { const onChange = vi.fn() - let renderer: ReturnType + let renderer!: ReturnType act(() => { renderer = create( createElement(NotificationDeliverySection, { value: DEFAULT_NOTIFICATION_DELIVERY, onChange }) diff --git a/mobile/src/notifications/android-foreground-push.test.ts b/mobile/src/notifications/android-foreground-push.test.ts index cad381e7ee9..2172dd5e339 100644 --- a/mobile/src/notifications/android-foreground-push.test.ts +++ b/mobile/src/notifications/android-foreground-push.test.ts @@ -1,5 +1,9 @@ import { beforeEach, expect, it, vi } from 'vitest' -import type { Notification } from 'expo-notifications' +import type { + FirebaseRemoteMessageNotification, + Notification, + NotificationTrigger +} from 'expo-notifications' import { startAndroidForegroundPushPresentation } from './android-foreground-push' const mocks = vi.hoisted(() => ({ @@ -19,26 +23,84 @@ vi.mock('expo-notifications', () => ({ scheduleNotificationAsync: mocks.schedule })) -function notification(trigger: unknown = { type: 'push', remoteMessage: { notification: null } }) { +// Expo's remote-message types are wide and fully required; the two builders below fill them once +// so the fixtures below can be plain `Notification` values rather than assertions. +const REMOTE_NOTIFICATION: FirebaseRemoteMessageNotification = { + body: null, + bodyLocalizationArgs: null, + bodyLocalizationKey: null, + channelId: null, + clickAction: null, + color: null, + eventTime: null, + icon: null, + imageUrl: null, + lightSettings: null, + link: null, + localOnly: false, + notificationCount: null, + notificationPriority: null, + sound: null, + sticky: false, + tag: null, + ticker: null, + title: null, + titleLocalizationArgs: null, + titleLocalizationKey: null, + usesDefaultLightSettings: false, + usesDefaultSound: false, + usesDefaultVibrateSettings: false, + vibrateTimings: null, + visibility: null +} + +function pushTrigger(remote: FirebaseRemoteMessageNotification | null): NotificationTrigger { return { + type: 'push', + remoteMessage: { + collapseKey: null, + data: {}, + from: null, + messageId: 'message-1', + messageType: null, + notification: remote, + originalPriority: 1, + priority: 1, + sentTime: 0, + to: null, + ttl: 0 + } + } +} + +const ORCA_PUSH_DATA: Record = { + hostFingerprint: 'host', + notificationId: 'event', + notificationEpoch: 'epoch', + notificationSeq: '3', + paneKey: 'pane', + channelId: 'orca-desktop' +} + +function notification( + trigger: NotificationTrigger = pushTrigger(null), + data: Record = ORCA_PUSH_DATA +): Notification { + return { + date: 0, request: { identifier: 'message-1', trigger, content: { title: 'Test notification', + subtitle: null, body: '', + categoryIdentifier: null, sound: 'default', - data: { - hostFingerprint: 'host', - notificationId: 'event', - notificationEpoch: 'epoch', - notificationSeq: '3', - paneKey: 'pane', - channelId: 'orca-desktop' - } + data } } - } as unknown as Notification + } } beforeEach(() => { @@ -52,9 +114,16 @@ it('presents a title-only data push with its original identity, routing and chan const incoming = notification() mocks.receive(incoming) await vi.waitFor(() => expect(mocks.schedule).toHaveBeenCalledOnce()) + // The four members the presenter forwards, named: it rebuilds content rather than passing the + // arriving object through, so comparing against the whole fixture would only hold by accident. expect(mocks.schedule).toHaveBeenCalledWith({ identifier: incoming.request.identifier, - content: incoming.request.content, + content: { + title: incoming.request.content.title, + body: incoming.request.content.body, + data: incoming.request.content.data, + sound: 'default' + }, trigger: { channelId: 'orca-desktop' } }) stop() @@ -64,18 +133,15 @@ it('presents a title-only data push with its original identity, routing and chan it('does not reschedule its own local notification or normal provider notifications', () => { startAndroidForegroundPushPresentation() mocks.receive(notification(null)) - mocks.receive(notification({ type: 'channel', channelId: 'orca-desktop' })) - mocks.receive(notification({ type: 'push', remoteMessage: { notification: { title: 'Test' } } })) + mocks.receive(notification({ channelId: 'orca-desktop' })) + mocks.receive(notification(pushTrigger({ ...REMOTE_NOTIFICATION, title: 'Test' }))) expect(mocks.schedule).not.toHaveBeenCalled() }) it('leaves silent dismissals and unrelated messages alone', () => { startAndroidForegroundPushPresentation() - const incoming = notification() - incoming.request.content.data.kind = 'dismiss' - mocks.receive(incoming) - incoming.request.content.data = {} - mocks.receive(incoming) + mocks.receive(notification(undefined, { ...ORCA_PUSH_DATA, kind: 'dismiss' })) + mocks.receive(notification(undefined, {})) expect(mocks.schedule).not.toHaveBeenCalled() }) diff --git a/mobile/src/notifications/mobile-notifications.test.ts b/mobile/src/notifications/mobile-notifications.test.ts index ba784520f98..8f85da25e25 100644 --- a/mobile/src/notifications/mobile-notifications.test.ts +++ b/mobile/src/notifications/mobile-notifications.test.ts @@ -2,6 +2,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { subscribeToDesktopNotifications } from './mobile-notifications' import { dismissHostPushNotification } from './push-socket-dismissal' import { requestNotificationCatchup } from './push-dismissal-reconciliation' +import { RpcClientStreamRegistry } from '../transport/rpc-client-stream-registry' +import type { RpcClient } from '../transport/rpc-client' +import type { RpcResponse } from '../transport/types' vi.mock('./push-socket-dismissal', () => ({ dismissHostPushNotification: vi.fn(async () => {}) @@ -13,6 +16,69 @@ vi.mock('./notification-permissions', () => ({})) type Handler = (data: unknown) => void +type SentFrame = { id: string; method: string; params: unknown } + +/** The registry sends through an `unknown` port, so name the shape the assertions read. */ +function readSentFrame(request: unknown): SentFrame { + if ( + typeof request !== 'object' || + request === null || + !('id' in request) || + typeof request.id !== 'string' || + !('method' in request) || + typeof request.method !== 'string' + ) { + throw new Error('The stream registry sent a frame without a string id and method') + } + return { + id: request.id, + method: request.method, + params: 'params' in request ? request.params : undefined + } +} + +/** The real stream registry, so dispose-before-ready is answered by the transport, not by a fake. */ +function registryClient() { + const sent: SentFrame[] = [] + const requests: { method: string; params: unknown }[] = [] + let id = 0 + const registry = new RpcClientStreamRegistry({ + nextId: () => `rpc-${++id}`, + deviceToken: 'device-token', + getState: () => 'connected', + sendEncrypted: (request) => { + sent.push(readSentFrame(request)) + return true + } + }) + const client: RpcClient = { + sendRequest: async (method, params) => { + requests.push({ method, params }) + return { id: 'reply-1', ok: true, result: {}, _meta: { runtimeId: 'runtime-1' } } + }, + subscribe: (method, params, onData, options) => + registry.subscribe(method, params, onData, options), + updateTerminalSubscriptionViewport: () => {}, + getState: () => 'connected', + getReconnectAttempt: () => 0, + getLastConnectedAt: () => null, + onStateChange: () => () => {}, + notifyForeground: () => {}, + close: () => {} + } + return { registry, sent, requests, client } +} + +function readyReply(id: string, subscriptionId: string): RpcResponse { + return { + id, + ok: true, + streaming: true, + result: { type: 'ready', subscriptionId }, + _meta: { runtimeId: 'runtime-1' } + } +} + function client() { let handler: Handler | undefined return { @@ -56,4 +122,32 @@ describe('subscribeToDesktopNotifications', () => { await Promise.resolve() expect(dismissHostPushNotification).toHaveBeenCalledWith(dismissal, 'host-1') }) + + it('never runs the ready arm when the disposer ran before the reply landed', () => { + const rpc = registryClient() + const stop = subscribeToDesktopNotifications(rpc.client, 'host-1') + const subscribeFrame = rpc.sent[0]! + expect(subscribeFrame.method).toBe('notifications.subscribe') + + stop() + rpc.registry.handleResponse(readyReply(subscribeFrame.id, 'sub-1')) + + expect(requestNotificationCatchup).not.toHaveBeenCalled() + // The subscription id never reaches this module, so nothing closes the host's stream. + expect(rpc.requests).toEqual([]) + expect(rpc.sent).toHaveLength(1) + }) + + it('closes the host stream when the disposer runs after the ready reply', async () => { + const rpc = registryClient() + const stop = subscribeToDesktopNotifications(rpc.client, 'host-1') + rpc.registry.handleResponse(readyReply(rpc.sent[0]!.id, 'sub-1')) + + stop() + await Promise.resolve() + + expect(rpc.requests).toEqual([ + { method: 'notifications.unsubscribe', params: { subscriptionId: 'sub-1' } } + ]) + }) }) diff --git a/mobile/src/notifications/mobile-notifications.ts b/mobile/src/notifications/mobile-notifications.ts index 48f8e86f63e..62ab9db4b05 100644 --- a/mobile/src/notifications/mobile-notifications.ts +++ b/mobile/src/notifications/mobile-notifications.ts @@ -29,13 +29,10 @@ export function subscribeToDesktopNotifications(client: RpcClient, hostId: strin const params = { includeDesktopSuppressed: true } const unsubscribeStream = client.subscribe('notifications.subscribe', params, (data: unknown) => { const event = data as DismissNotificationEvent | SubscribeResult | { type: string } + // No dispose-before-ready arm: every transport detaches this listener inside + // `unsubscribeStream()`, so a callback that runs at all runs before disposal. if (event.type === 'ready') { subscriptionId = (event as SubscribeResult).subscriptionId - if (disposed) { - unsubscribeServer(subscriptionId) - unsubscribeStream() - return - } // A max watermark asks only which delivered pushes are stale; socket history // never becomes a second OS-notification delivery route. void requestNotificationCatchup(client, hostId, () => disposed).catch(() => {}) diff --git a/mobile/src/notifications/notification-reply-schema.test.ts b/mobile/src/notifications/notification-reply-schema.test.ts index fe298b279ff..1e6ae74ff09 100644 --- a/mobile/src/notifications/notification-reply-schema.test.ts +++ b/mobile/src/notifications/notification-reply-schema.test.ts @@ -3,6 +3,8 @@ import type { z } from 'zod' import { missedNotificationsSchema, notificationUnreadReplySchema, + PUSH_REGISTER_REFUSAL_REASONS, + PUSH_TEST_REFUSAL_REASONS, pushDeliveryTestResultSchema, pushRouteRegistrationSchema } from './notification-reply-schema' @@ -73,6 +75,17 @@ describe('closed enums degrade to the copy main showed', () => { reads(pushRouteRegistrationSchema, { registered: false, reason: 'moon-phase' })?.reason ).toBe(undefined) }) + + // Both arm lists are pinned to the host's refusal unions in the schema module, where tsc looks; + // these loops prove every pinned arm survives the parse, not just the ones picked above. + it('keeps every refusal reason the host declares for either route', () => { + for (const reason of PUSH_TEST_REFUSAL_REASONS) { + expect(reads(pushDeliveryTestResultSchema, { accepted: false, reason })?.reason).toBe(reason) + } + for (const reason of PUSH_REGISTER_REFUSAL_REASONS) { + expect(reads(pushRouteRegistrationSchema, { registered: false, reason })?.reason).toBe(reason) + } + }) }) describe('dismissal rows stay opaque', () => { diff --git a/mobile/src/notifications/notification-reply-schema.ts b/mobile/src/notifications/notification-reply-schema.ts index 2432c727487..6cb3ef3e510 100644 --- a/mobile/src/notifications/notification-reply-schema.ts +++ b/mobile/src/notifications/notification-reply-schema.ts @@ -1,5 +1,9 @@ import { z } from 'zod' -import { salvagedOptional } from '../../../src/shared/zod-salvage' +import type { + MobilePushRegisterResult, + MobilePushTestResult +} from '../../../src/shared/mobile-push-contract' +import { hostUnionArms, salvagedOptional } from '../../../src/shared/zod-salvage' // The four notification replies mobile reads: the push-route register/unregister pair, the // settings screen's delivery probe, and the tray catch-up. Checked against the handlers in @@ -20,6 +24,26 @@ import { salvagedOptional } from '../../../src/shared/zod-salvage' */ export const notificationUnreadReplySchema = z.unknown() +// Both reason vocabularies are pinned to the host's own refusal arms through hostUnionArms, so an +// arm added or dropped host-side fails tsc here instead of degrading silently on the phone. +export const PUSH_TEST_REFUSAL_REASONS = hostUnionArms< + Extract['reason'] +>({ + not_registered: true, + unavailable: true, + rate_limited: true, + rejected: true +}) +export const PUSH_REGISTER_REFUSAL_REASONS = hostUnionArms< + Extract['reason'] +>({ + gateway_unreachable: true, + gateway_rejected: true, + not_mobile: true, + registration_storage_failed: true, + throttled: true +}) + /** * Whether Orca's push service took a test notification. * @@ -28,7 +52,7 @@ export const notificationUnreadReplySchema = z.unknown() * `reason` is a closed enum because those two comparisons are the whole of what it decides — an arm * this build does not know degrades to the generic "Could not send" copy, which is the arm main took * for every unrecognised string too — pinned by the `notifications-display-test-unknown-reason` - * golden. The arms are the host's own (mobile-push-contract.ts:99). + * golden. The arms are the host's own (mobile-push-contract.ts:99), pinned to it above. * * Total, so a result that is not an object degrades the same way. A refusal here would not be * silent: the call site's `try` turns it into the reader's own sentence in the message slot where @@ -38,10 +62,7 @@ export const notificationUnreadReplySchema = z.unknown() export const pushDeliveryTestResultSchema = z .looseObject({ accepted: salvagedOptional('accepted', z.boolean()), - reason: salvagedOptional( - 'reason', - z.enum(['not_registered', 'unavailable', 'rate_limited', 'rejected']) - ) + reason: salvagedOptional('reason', z.enum(PUSH_TEST_REFUSAL_REASONS)) }) .nullish() .catch(undefined) @@ -52,23 +73,14 @@ export const pushDeliveryTestResultSchema = z * `registered` is the only member read — push-registration.ts:117 compares it to `true` through * `?.` on a payload main already typed as nullable — so the reply stays nullish and every member * optional. `registrationId` and `reason` are declared because the host sends them - * (MobilePushRegisterResult, mobile-push-contract.ts:38-49, whose five reason arms these are) and a - * future reader should find them here rather than re-assert them. + * (MobilePushRegisterResult, mobile-push-contract.ts:38-49, whose five reason arms these are, + * pinned to it above) and a future reader should find them here rather than re-assert them. */ export const pushRouteRegistrationSchema = z .looseObject({ registered: salvagedOptional('registered', z.boolean()), registrationId: salvagedOptional('registrationId', z.string()), - reason: salvagedOptional( - 'reason', - z.enum([ - 'gateway_unreachable', - 'gateway_rejected', - 'not_mobile', - 'registration_storage_failed', - 'throttled' - ]) - ) + reason: salvagedOptional('reason', z.enum(PUSH_REGISTER_REFUSAL_REASONS)) }) .nullish() diff --git a/mobile/src/session/MobileNativeChatComposer.tsx b/mobile/src/session/MobileNativeChatComposer.tsx index 20c43aa6c8b..29efb22f547 100644 --- a/mobile/src/session/MobileNativeChatComposer.tsx +++ b/mobile/src/session/MobileNativeChatComposer.tsx @@ -60,7 +60,7 @@ type Props = { onMicPress?: () => void micActive?: boolean /** Dictation trigger style — 'hold' uses press-in/out, 'toggle' uses tap. */ - dictationMode?: 'toggle' | 'hold' + dictationMode?: string onMicPressIn?: () => void onMicPressOut?: () => void disabled?: boolean diff --git a/mobile/src/session/MobileNativeChatOverlay.tsx b/mobile/src/session/MobileNativeChatOverlay.tsx index a72b91ff29c..65f25e4ea24 100644 --- a/mobile/src/session/MobileNativeChatOverlay.tsx +++ b/mobile/src/session/MobileNativeChatOverlay.tsx @@ -16,7 +16,7 @@ type Props = { images: MobileNativeChatImageAttachments onMicPress: () => void micActive: boolean - dictationMode: 'toggle' | 'hold' + dictationMode: string | undefined onMicPressIn: () => void onMicPressOut: () => void inputLockReason: MobileNativeChatInputLockReason | null diff --git a/mobile/src/session/MobileNativeChatView.test.ts b/mobile/src/session/MobileNativeChatView.test.ts index a245a1fe7bb..6b80c3500ae 100644 --- a/mobile/src/session/MobileNativeChatView.test.ts +++ b/mobile/src/session/MobileNativeChatView.test.ts @@ -262,7 +262,9 @@ describe('MobileNativeChatView', () => { try { const folded = [assistantTurn('a1', 'Starting')] await render({ folded }) - await act(async () => vi.runOnlyPendingTimers()) + await act(async () => { + vi.runOnlyPendingTimers() + }) scrollToEnd.mockClear() await update({ folded, streaming: 'Streaming output' }) @@ -270,7 +272,9 @@ describe('MobileNativeChatView', () => { expect(scrollToOffset).toHaveBeenCalledOnce() expect(scrollToOffset).toHaveBeenLastCalledWith({ animated: false, offset: 900 }) - await act(async () => vi.advanceTimersByTime(60)) + await act(async () => { + vi.advanceTimersByTime(60) + }) expect(scrollToOffset).toHaveBeenCalledOnce() } finally { vi.useRealTimers() @@ -353,7 +357,9 @@ describe('MobileNativeChatView', () => { list().props.onContentSizeChange(320, 1_250) list().props.onMomentumScrollBegin?.({}) }) - await act(async () => vi.advanceTimersByTime(200)) + await act(async () => { + vi.advanceTimersByTime(200) + }) act(() => list().props.onContentSizeChange(320, 1_300)) expect(scrollToEnd).not.toHaveBeenCalled() @@ -439,7 +445,9 @@ describe('MobileNativeChatView', () => { } }) }) - await act(async () => vi.advanceTimersByTime(200)) + await act(async () => { + vi.advanceTimersByTime(200) + }) act(() => list().props.onContentSizeChange(320, 1_250)) expect(scrollToEnd).toHaveBeenCalledOnce() @@ -471,7 +479,9 @@ describe('MobileNativeChatView', () => { expect(scrollToEnd).not.toHaveBeenCalled() expect(scrollToOffset).not.toHaveBeenCalled() - await act(async () => vi.runOnlyPendingTimers()) + await act(async () => { + vi.runOnlyPendingTimers() + }) expect(scrollToEnd).toHaveBeenCalledOnce() expect(scrollToEnd).toHaveBeenLastCalledWith({ animated: false }) @@ -521,7 +531,9 @@ describe('MobileNativeChatView', () => { } }) }) - await act(async () => vi.advanceTimersByTime(200)) + await act(async () => { + vi.advanceTimersByTime(200) + }) act(() => list().props.onContentSizeChange(320, 950)) expect(onLoadEarlier).toHaveBeenCalledOnce() @@ -540,7 +552,9 @@ describe('MobileNativeChatView', () => { try { const folded = [assistantTurn('a1', 'History')] await render({ folded }) - await act(async () => vi.runOnlyPendingTimers()) + await act(async () => { + vi.runOnlyPendingTimers() + }) await scrollAwayFromTail() scrollToEnd.mockClear() @@ -548,7 +562,9 @@ describe('MobileNativeChatView', () => { expect(scrollToEnd).toHaveBeenCalledOnce() expect(scrollToEnd).toHaveBeenLastCalledWith({ animated: false }) - await act(async () => vi.advanceTimersByTime(60)) + await act(async () => { + vi.advanceTimersByTime(60) + }) expect(scrollToEnd).toHaveBeenCalledOnce() } finally { vi.useRealTimers() @@ -577,7 +593,9 @@ describe('MobileNativeChatView', () => { try { const folded = [assistantTurn('a1', 'Latest')] await render({ folded }) - await act(async () => vi.runOnlyPendingTimers()) + await act(async () => { + vi.runOnlyPendingTimers() + }) scrollToEnd.mockClear() await update({ folded, keyboardInset: 320 }) @@ -585,7 +603,9 @@ describe('MobileNativeChatView', () => { expect(scrollToEnd).toHaveBeenCalledOnce() expect(scrollToEnd).toHaveBeenLastCalledWith({ animated: false }) - await act(async () => vi.advanceTimersByTime(60)) + await act(async () => { + vi.advanceTimersByTime(60) + }) expect(scrollToEnd).toHaveBeenCalledOnce() await scrollAwayFromTail() @@ -611,14 +631,20 @@ describe('MobileNativeChatView', () => { vi.useFakeTimers() try { await render({ inputLockReason: 'waiting' }) - await act(async () => vi.advanceTimersByTime(600)) + await act(async () => { + vi.advanceTimersByTime(600) + }) expect(composer().props.disabled).toBe(true) await update({ inputLockReason: null }) expect(composer().props.disabled).toBe(true) - await act(async () => vi.advanceTimersByTime(300)) + await act(async () => { + vi.advanceTimersByTime(300) + }) await update({ inputLockReason: 'waiting' }) - await act(async () => vi.advanceTimersByTime(600)) + await act(async () => { + vi.advanceTimersByTime(600) + }) expect(composer().props.disabled).toBe(true) expect(composer().props.placeholder).toBe('Waiting for terminal…') @@ -631,12 +657,18 @@ describe('MobileNativeChatView', () => { vi.useFakeTimers() try { await render({ inputLockReason: 'waiting' }) - await act(async () => vi.advanceTimersByTime(600)) + await act(async () => { + vi.advanceTimersByTime(600) + }) await update({ inputLockReason: null }) - await act(async () => vi.advanceTimersByTime(599)) + await act(async () => { + vi.advanceTimersByTime(599) + }) expect(composer().props.disabled).toBe(true) - await act(async () => vi.advanceTimersByTime(1)) + await act(async () => { + vi.advanceTimersByTime(1) + }) expect(composer().props.disabled).toBe(false) expect(composer().props.placeholder).toBe('Message, @files, /commands') diff --git a/mobile/src/session/MobileNativeChatView.tsx b/mobile/src/session/MobileNativeChatView.tsx index 49f20fa5d5d..6a51601d151 100644 --- a/mobile/src/session/MobileNativeChatView.tsx +++ b/mobile/src/session/MobileNativeChatView.tsx @@ -93,7 +93,7 @@ type Props = { isAttaching?: boolean onMicPress?: () => void micActive?: boolean - dictationMode?: 'toggle' | 'hold' + dictationMode?: string onMicPressIn?: () => void onMicPressOut?: () => void inputLockReason?: MobileNativeChatInputLockReason | null diff --git a/mobile/src/session/MobileTerminalInputActions.tsx b/mobile/src/session/MobileTerminalInputActions.tsx index 4a42b327832..37f69ae3bb3 100644 --- a/mobile/src/session/MobileTerminalInputActions.tsx +++ b/mobile/src/session/MobileTerminalInputActions.tsx @@ -12,7 +12,7 @@ type MobileTerminalInputActionsProps = { readonly canSend: boolean readonly isAttaching: boolean readonly dictation: DictationState - readonly dictationMode: 'toggle' | 'hold' + readonly dictationMode: string | undefined readonly buttonStyle: StyleProp readonly activeButtonStyle: StyleProp readonly disabledButtonStyle: StyleProp diff --git a/mobile/src/session/diff-review-reply-schema.ts b/mobile/src/session/diff-review-reply-schema.ts index b35da5da76b..b875df72b05 100644 --- a/mobile/src/session/diff-review-reply-schema.ts +++ b/mobile/src/session/diff-review-reply-schema.ts @@ -27,6 +27,9 @@ const GIT_BRANCH_COMPARE_STATUS = [ type GitBranchCompareStatus = (typeof GIT_BRANCH_COMPARE_STATUS)[number] +// Built once: readProjectedCompareStatus runs on every reply. +const gitBranchCompareStatusSchema = z.enum(GIT_BRANCH_COMPARE_STATUS) + /** * One committed change. * @@ -101,7 +104,7 @@ export const branchCompareProjectionSchema: z.ZodType { kind: 'advance', draft: { promptKey: PROMPT_KEY, answers: [{ questionId: 'q1', optionIds: ['q1:choice-1'] }] } }) + // Read it non-optionally: an absent advance must fail here, not fall to the null draft and + // leave the assertion below describing the first step again. + expect(advance).toBeDefined() const second = projectGroupedQuestion( questions, - advance!.kind === 'advance' ? advance.draft : null, + advance!.kind === 'advance' ? advance!.draft : null, PROMPT_KEY ) expect(second).toMatchObject({ question: 'Which regions? (2 of 2)', multiSelect: true }) diff --git a/mobile/src/session/mobile-terminal-records.test.ts b/mobile/src/session/mobile-terminal-records.test.ts index bcc9510d0e8..efd368411f4 100644 --- a/mobile/src/session/mobile-terminal-records.test.ts +++ b/mobile/src/session/mobile-terminal-records.test.ts @@ -172,7 +172,7 @@ describe('mobile terminal records', () => { ...base, agentStatus: { ...base.agentStatus!, - state: 'blocked', + state: 'blocked' as const, updatedAt: 2, stateStartedAt: 2 } diff --git a/mobile/src/session/use-mobile-native-chat-answer-send.test.ts b/mobile/src/session/use-mobile-native-chat-answer-send.test.ts index cfb35417e9f..394c5555e2f 100644 --- a/mobile/src/session/use-mobile-native-chat-answer-send.test.ts +++ b/mobile/src/session/use-mobile-native-chat-answer-send.test.ts @@ -138,11 +138,17 @@ describe('useMobileNativeChatAnswerSend', () => { expect(sendRequest).toHaveBeenCalledTimes(1) expect(sendRequest.mock.calls[0]?.[1]).toMatchObject({ text: '1', enter: false }) - await act(async () => vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS)) + await act(async () => { + await vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS) + }) expect(sendRequest.mock.calls[1]?.[1]).toMatchObject({ text: '3', enter: false }) - await act(async () => vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS)) + await act(async () => { + await vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS) + }) expect(sendRequest.mock.calls[2]?.[1]).toMatchObject({ text: '\x1b[C', enter: false }) - await act(async () => vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS)) + await act(async () => { + await vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS) + }) await expect(result).resolves.toBe(true) expect(sendRequest.mock.calls[3]?.[1]).toMatchObject({ text: '\r', enter: false }) }) @@ -161,7 +167,9 @@ describe('useMobileNativeChatAnswerSend', () => { await act(async () => { result = answerSend?.answerAsk(prompt, [{ indices: [1] }, { indices: [0] }]) }) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) await expect(result).resolves.toBe(true) expect(sendRequest.mock.calls.map((call) => call[1])).toEqual([ @@ -191,8 +199,12 @@ describe('useMobileNativeChatAnswerSend', () => { await act(async () => { result = answerSend?.answerAsk(prompt, [{ indices: [1] }, { indices: [0] }]) }) - await act(async () => vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS)) - await act(async () => vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS)) + await act(async () => { + await vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS) + }) + await act(async () => { + await vi.advanceTimersByTimeAsync(MOBILE_NATIVE_CHAT_QUESTION_STEP_MS) + }) await expect(result).resolves.toBe(true) // 15s total transport, minus 6s per completed write; the 1s pacing steps are @@ -209,7 +221,9 @@ describe('useMobileNativeChatAnswerSend', () => { // A newline in raw keystrokes would submit early — must collapse to space. result = answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [], other: 'zeta\nspaces' }]) }) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) await expect(result).resolves.toBe(true) expect(sendRequest.mock.calls.map((call) => call[1])).toEqual([ @@ -252,7 +266,9 @@ describe('useMobileNativeChatAnswerSend', () => { await act(async () => { result = answerSend?.answerAsk(prompt, [{ indices: [1] }, { indices: [0] }]) }) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) await expect(result).resolves.toBe(true) expect(sendRequest.mock.calls.map((call) => call[1])).toEqual([ @@ -354,7 +370,9 @@ describe('useMobileNativeChatAnswerSend', () => { await act(async () => { result = answerSend?.answerAsk(prompt, [{ indices: [1] }, { indices: [0] }]) }) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) await expect(result).resolves.toBe(false) // The first group DID land, so the remote selector is half-stepped — telling the @@ -398,7 +416,9 @@ describe('useMobileNativeChatAnswerSend', () => { expect(sendRequest).toHaveBeenCalledTimes(1) await setEnabled(false) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) await expect(result).resolves.toBe(false) expect(sendRequest).toHaveBeenCalledTimes(1) @@ -455,7 +475,9 @@ describe('useMobileNativeChatAnswerSend', () => { await act(async () => { second = answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }]) }) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) await expect(first).resolves.toBe(false) await expect(second).resolves.toBe(false) diff --git a/mobile/src/session/use-mobile-native-chat-controller.test.ts b/mobile/src/session/use-mobile-native-chat-controller.test.ts index 00eee9651a0..0bffa3039f2 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.test.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.test.ts @@ -33,6 +33,7 @@ const structuredOptionSnapshot: SessionOptionDescriptor[] = [ choices: [{ value: 'gpt-fast', label: 'GPT Fast' }] }, valueSource: 'reported', + transport: 'agent-session', settable: true } ] diff --git a/mobile/src/session/use-mobile-native-chat-drafts.test.ts b/mobile/src/session/use-mobile-native-chat-drafts.test.ts index 586b384e770..2547d9191be 100644 --- a/mobile/src/session/use-mobile-native-chat-drafts.test.ts +++ b/mobile/src/session/use-mobile-native-chat-drafts.test.ts @@ -219,7 +219,9 @@ describe('useMobileNativeChatDrafts', () => { // A relay drop can stall the transcript stream past the deadline; the // delivered prompt must not reappear in the composer when it recovers. - act(() => vi.advanceTimersByTime(25_000)) + act(() => { + vi.advanceTimersByTime(25_000) + }) await act(async () => renderer?.update( createElement(Harness, { tabId: 'a', messages: [userTextMessage('m1', 'ping')] }) @@ -510,7 +512,9 @@ describe('useMobileNativeChatDrafts', () => { ) ) - act(() => vi.advanceTimersByTime(30_000)) + act(() => { + vi.advanceTimersByTime(30_000) + }) expect(onUnconfirmed).not.toHaveBeenCalled() } finally { vi.useRealTimers() @@ -557,7 +561,9 @@ describe('useMobileNativeChatDrafts', () => { }) ) ) - act(() => vi.advanceTimersByTime(30_000)) + act(() => { + vi.advanceTimersByTime(30_000) + }) expect(onUnconfirmed).not.toHaveBeenCalled() } finally { vi.useRealTimers() @@ -614,7 +620,9 @@ describe('useMobileNativeChatDrafts', () => { }) expect(vi.getTimerCount()).toBe(0) - act(() => vi.advanceTimersByTime(30_000)) + act(() => { + vi.advanceTimersByTime(30_000) + }) expect(onUnconfirmed).not.toHaveBeenCalled() } finally { vi.useRealTimers() @@ -633,9 +641,13 @@ describe('useMobileNativeChatDrafts', () => { } }) - act(() => vi.advanceTimersByTime(19_999)) + act(() => { + vi.advanceTimersByTime(19_999) + }) expect(onUnconfirmed).not.toHaveBeenCalled() - act(() => vi.advanceTimersByTime(1)) + act(() => { + vi.advanceTimersByTime(1) + }) expect(onUnconfirmed).toHaveBeenCalledTimes(1) } finally { vi.useRealTimers() @@ -670,7 +682,9 @@ describe('useMobileNativeChatDrafts', () => { ) expect(state?.composerText).toBe('ping') - act(() => vi.advanceTimersByTime(30_000)) + act(() => { + vi.advanceTimersByTime(30_000) + }) expect(onUnconfirmed).toHaveBeenCalledTimes(1) } finally { vi.useRealTimers() @@ -704,7 +718,9 @@ describe('useMobileNativeChatDrafts', () => { ) expect(state?.composerText).toBe('ping') - act(() => vi.advanceTimersByTime(30_000)) + act(() => { + vi.advanceTimersByTime(30_000) + }) expect(onUnconfirmed).toHaveBeenCalledTimes(1) } finally { vi.useRealTimers() @@ -731,7 +747,9 @@ describe('useMobileNativeChatDrafts', () => { createElement(Harness, { tabId: 'a', messages: [userTextMessage('echo-1', 'ping')] }) ) ) - act(() => vi.advanceTimersByTime(30_000)) + act(() => { + vi.advanceTimersByTime(30_000) + }) expect(firstUnconfirmed).not.toHaveBeenCalled() expect(secondUnconfirmed).toHaveBeenCalledTimes(1) @@ -757,7 +775,9 @@ describe('useMobileNativeChatDrafts', () => { }) expect(vi.getTimerCount()).toBe(0) - act(() => vi.advanceTimersByTime(30_000)) + act(() => { + vi.advanceTimersByTime(30_000) + }) expect(onUnconfirmed).not.toHaveBeenCalled() } finally { vi.useRealTimers() @@ -807,7 +827,9 @@ describe('useMobileNativeChatDrafts', () => { ) expect(state?.composerText).toBe('ping') - act(() => vi.advanceTimersByTime(30_000)) + act(() => { + vi.advanceTimersByTime(30_000) + }) expect(onUnconfirmed).toHaveBeenCalledTimes(1) } finally { vi.useRealTimers() diff --git a/mobile/src/session/use-mobile-native-chat-file-search.test.ts b/mobile/src/session/use-mobile-native-chat-file-search.test.ts index f815eb88793..725bd591653 100644 --- a/mobile/src/session/use-mobile-native-chat-file-search.test.ts +++ b/mobile/src/session/use-mobile-native-chat-file-search.test.ts @@ -56,10 +56,14 @@ describe('useMobileNativeChatFileSearch', () => { state?.loadNativeChatFiles('a') state?.loadNativeChatFiles('app') }) - await act(async () => vi.advanceTimersByTimeAsync(119)) + await act(async () => { + await vi.advanceTimersByTimeAsync(119) + }) expect(sendRequest).not.toHaveBeenCalled() - await act(async () => vi.advanceTimersByTimeAsync(1)) + await act(async () => { + await vi.advanceTimersByTimeAsync(1) + }) expect(sendRequest).toHaveBeenCalledTimes(1) expect(sendRequest).toHaveBeenCalledWith('files.searchPaths', { worktree: 'id:wt-1', @@ -84,11 +88,15 @@ describe('useMobileNativeChatFileSearch', () => { await mount(fakeClient({ sendRequest })) act(() => state?.loadNativeChatFiles('apple')) - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) expect(state?.nativeChatFilePaths).toEqual(['src/apple.ts']) act(() => state?.loadNativeChatFiles('readme')) - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) expect(state?.nativeChatFilePaths).toEqual(['docs/readme.md']) expect(sendRequest.mock.calls.map(([method]) => method)).toEqual([ 'files.searchPaths', @@ -104,7 +112,9 @@ describe('useMobileNativeChatFileSearch', () => { // Populate the cache for 'app'. act(() => state?.loadNativeChatFiles('app')) - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) expect(state?.nativeChatFilePaths).toEqual(['src/app.ts']) // Schedule 'beta' (debounced, unresolved), then hit the cache for 'app'. @@ -115,7 +125,9 @@ describe('useMobileNativeChatFileSearch', () => { expect(state?.nativeChatFilePaths).toEqual(['src/app.ts']) // The cancelled 'beta' request must never fire and overwrite the cached result. - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) expect(state?.nativeChatFilePaths).toEqual(['src/app.ts']) expect( sendRequest.mock.calls.filter(([, params]) => (params as { query: string }).query === 'beta') @@ -141,13 +153,17 @@ describe('useMobileNativeChatFileSearch', () => { const listCalls = (): number => sendRequest.mock.calls.filter(([method]) => method === 'files.list').length act(() => state?.loadNativeChatFiles('apple')) - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) expect(state?.nativeChatFilePaths).toEqual(['src/apple.ts']) expect(listCalls()).toBe(1) // Control: a fresh query under the same epoch is answered from the inventory already held. act(() => state?.loadNativeChatFiles('readme')) - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) expect(listCalls()).toBe(1) // `migrateTo` advanced the logical authority epoch. The client is the same object and the @@ -155,7 +171,9 @@ describe('useMobileNativeChatFileSearch', () => { // inventory the host under the old authority gave us. generation = 2 act(() => state?.loadNativeChatFiles('guide')) - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) expect(listCalls()).toBe(2) expect(state?.nativeChatFilePaths).toEqual(['docs/guide.md']) }) @@ -179,9 +197,13 @@ describe('useMobileNativeChatFileSearch', () => { await mount(fakeClient({ sendRequest })) act(() => state?.loadNativeChatFiles('apple')) - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) act(() => state?.loadNativeChatFiles('readme')) - await act(async () => vi.advanceTimersByTimeAsync(120)) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) expect(sendRequest.mock.calls.filter(([method]) => method === 'files.list')).toHaveLength(1) await act(async () => { diff --git a/mobile/src/session/use-mobile-native-chat-input-lease.test.ts b/mobile/src/session/use-mobile-native-chat-input-lease.test.ts index 6e4d12a2bcc..792e2b66d38 100644 --- a/mobile/src/session/use-mobile-native-chat-input-lease.test.ts +++ b/mobile/src/session/use-mobile-native-chat-input-lease.test.ts @@ -33,7 +33,9 @@ describe('useMobileNativeChatInputLease', () => { expect(lease?.ready).toBe(true) expect(lease?.lockReason).toBeNull() - act(() => lease?.clear()) + act(() => { + lease?.clear() + }) expect(lease?.ready).toBe(false) act(() => lease?.markReady('terminal')) expect(lease?.ready).toBe(true) @@ -86,18 +88,26 @@ describe('useSettledMobileNativeChatInputLock', () => { renderer = create(createElement(Harness, { reason: 'waiting' })) }) expect(settled).toBeNull() - act(() => vi.advanceTimersByTime(600)) + act(() => { + vi.advanceTimersByTime(600) + }) expect(settled).toBe('waiting') // A brief unlock that reverts inside the settle window never reaches the composer. act(() => renderer?.update(createElement(Harness, { reason: null }))) - act(() => vi.advanceTimersByTime(300)) + act(() => { + vi.advanceTimersByTime(300) + }) act(() => renderer?.update(createElement(Harness, { reason: 'disconnected' }))) - act(() => vi.advanceTimersByTime(600)) + act(() => { + vi.advanceTimersByTime(600) + }) expect(settled).toBe('disconnected') act(() => renderer?.update(createElement(Harness, { reason: null }))) - act(() => vi.advanceTimersByTime(600)) + act(() => { + vi.advanceTimersByTime(600) + }) expect(settled).toBeNull() }) }) diff --git a/mobile/src/session/use-mobile-native-chat-stop.test.ts b/mobile/src/session/use-mobile-native-chat-stop.test.ts index a1ee9ab4dd9..fa6a69749da 100644 --- a/mobile/src/session/use-mobile-native-chat-stop.test.ts +++ b/mobile/src/session/use-mobile-native-chat-stop.test.ts @@ -75,7 +75,9 @@ describe('useMobileNativeChatStop', () => { expect(sendRequest).toHaveBeenCalledTimes(1) await render(enabled as boolean, streamIdentity as string) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) expect(sendRequest).toHaveBeenCalledTimes(1) }) @@ -102,7 +104,9 @@ describe('useMobileNativeChatStop', () => { await render(true, 'stream-1') act(() => stop?.()) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) expect(onSendError).toHaveBeenCalledOnce() expect(onSendError).toHaveBeenCalledWith('Stop not sent') @@ -183,7 +187,9 @@ describe('useMobileNativeChatStop', () => { act(() => stop?.()) act(() => stop?.()) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) await act(async () => { rejectFirst(new Error('late failure')) await Promise.resolve() @@ -196,7 +202,9 @@ describe('useMobileNativeChatStop', () => { await render(true, 'stream-1') act(() => stop?.()) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) expect(reportWorkerTerminalUserInput).toHaveBeenCalledWith( expect.objectContaining({ sendRequest }), @@ -209,7 +217,9 @@ describe('useMobileNativeChatStop', () => { await render(true, 'stream-1') act(() => stop?.()) - await act(async () => vi.runAllTimersAsync()) + await act(async () => { + await vi.runAllTimersAsync() + }) expect(reportWorkerTerminalUserInput).not.toHaveBeenCalled() }) diff --git a/mobile/src/session/use-mobile-session-document-readers.ts b/mobile/src/session/use-mobile-session-document-readers.ts index 1da354f431d..857d1981570 100644 --- a/mobile/src/session/use-mobile-session-document-readers.ts +++ b/mobile/src/session/use-mobile-session-document-readers.ts @@ -52,12 +52,7 @@ export function useMobileSessionDocumentReaders(scope: MobileSessionTabApplicati if (!fallback.accepted) { throw new Error('Unable to read markdown') } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: main cast this payload unread; the shared preview reader hands it back whole. - const fileResult = fallback.value as { - content: string - truncated: boolean - byteLength: number - } + const fileResult = fallback.value setMarkdownDocs((prev) => new Map(prev).set( tab.id, diff --git a/mobile/src/session/use-mobile-session-screen-state.ts b/mobile/src/session/use-mobile-session-screen-state.ts index 6e2f82124b3..2d7596d8971 100644 --- a/mobile/src/session/use-mobile-session-screen-state.ts +++ b/mobile/src/session/use-mobile-session-screen-state.ts @@ -119,7 +119,9 @@ export function useMobileSessionScreenState(scope: MobileSessionFoundationModel) const [canPaste, setCanPaste] = useState(false) const [showDictationSetup, setShowDictationSetup] = useState(false) // 'hold' = press-and-hold mic, 'toggle' = tap-to-start/stop; mirrors Settings ▸ Voice ▸ Dictation Mode. - const [dictationMode, setDictationMode] = useState<'toggle' | 'hold'>('toggle') + // Holds the host's spelling verbatim, and undefined once a setup reply arrives without one: only + // the two arms below bind mic handlers, so anything else leaves the button as inert as it starts. + const [dictationMode, setDictationMode] = useState('toggle') const [toastMessage, setToastMessage] = useState(null) const toastOpacityRef = useRef(new Animated.Value(0)) const toastHideTimerRef = useRef | null>(null) diff --git a/mobile/src/session/use-throttled-latest-value.test.ts b/mobile/src/session/use-throttled-latest-value.test.ts index 29e58c8fa35..b439f394d77 100644 --- a/mobile/src/session/use-throttled-latest-value.test.ts +++ b/mobile/src/session/use-throttled-latest-value.test.ts @@ -43,7 +43,9 @@ describe('useThrottledLatestValue', () => { update('ab') update('abc') expect(latest).toBe('a') - act(() => vi.advanceTimersByTime(50)) + act(() => { + vi.advanceTimersByTime(50) + }) expect(latest).toBe('abc') }) @@ -53,7 +55,9 @@ describe('useThrottledLatestValue', () => { expect(latest).toBe('a') update(undefined) expect(latest).toBeUndefined() - act(() => vi.advanceTimersByTime(50)) + act(() => { + vi.advanceTimersByTime(50) + }) expect(latest).toBeUndefined() }) }) diff --git a/mobile/src/source-control/git-status-reply-schema.ts b/mobile/src/source-control/git-status-reply-schema.ts index 453b5f843fb..68c0bc802fb 100644 --- a/mobile/src/source-control/git-status-reply-schema.ts +++ b/mobile/src/source-control/git-status-reply-schema.ts @@ -24,6 +24,9 @@ const GIT_CONFLICT_KIND = [ ] as const const GIT_CONFLICT_OPERATION = ['merge', 'rebase', 'cherry-pick', 'unknown'] as const +// Built once: readProjectedConflictOperation runs on every reply. +const gitConflictOperationSchema = z.enum(GIT_CONFLICT_OPERATION) + /** * One working-tree entry. * @@ -70,7 +73,7 @@ const gitUpstreamStatusSchema = z.looseObject({ */ export const gitStatusHostPayloadSchema = z.looseObject({ entries: salvagingArray(gitStatusEntrySchema), - conflictOperation: salvagedOptional('conflictOperation', z.enum(GIT_CONFLICT_OPERATION)), + conflictOperation: salvagedOptional('conflictOperation', gitConflictOperationSchema), branch: salvagedOptional('branch', z.string()), head: salvagedOptional('head', z.string()), upstreamStatus: salvagedOptional('upstreamStatus', gitUpstreamStatusSchema) @@ -168,6 +171,6 @@ export const gitStatusProjectionSchema: z.ZodType { + it('publishes a ready compare whatever the previous state was', () => { + const outcome: BranchCompareOutcome = { kind: 'ready', result: REPLY } + expect(nextBranchCompareState(outcome, { kind: 'idle' }, false)).toEqual(READY) + expect(nextBranchCompareState(outcome, { kind: 'error', message: 'old' }, true)).toEqual(READY) + }) + + it('keeps a prior ready compare only when the caller asked for it', () => { + expect(nextBranchCompareState(FAILED, READY, true)).toBe(READY) + expect(nextBranchCompareState(UNAVAILABLE, READY, true)).toBe(READY) + expect(nextBranchCompareState(FAILED, READY, false)).toEqual({ + kind: 'error', + message: 'no base' + }) + }) + + it('separates a host without git from an attempt that failed', () => { + expect(nextBranchCompareState(UNAVAILABLE, { kind: 'loading' }, false)).toEqual({ + kind: 'idle' + }) + expect(nextBranchCompareState(FAILED, { kind: 'loading' }, false)).toEqual({ + kind: 'error', + message: 'no base' + }) + }) +}) diff --git a/mobile/src/source-control/mobile-branch-compare-outcome.ts b/mobile/src/source-control/mobile-branch-compare-outcome.ts new file mode 100644 index 00000000000..6ecaeea34aa --- /dev/null +++ b/mobile/src/source-control/mobile-branch-compare-outcome.ts @@ -0,0 +1,84 @@ +import type { RpcOperationSender } from '../transport/rpc-operation-sender' +import type { RequestCurrency } from '../transport/generation-scoped-request-owner' +import { refusedRpcMessageOrFallback } from '../transport/rpc-refusal-message' +import { resolveMobileBranchCompareBaseRef } from './mobile-branch-base-ref' +import { gitBranchCompareRead } from './mobile-git-read-operations' +import { isMobileGitUnavailableReply } from './mobile-git-status' +import type { MobileGitBranchCompareReply } from './git-compare-reply-schema' +import type { MobileBranchCompareState } from './mobile-source-control-screen-state' + +// The compare leg's own protocol and screen mapping: what one attempt against the worktree's base +// can end as, and what each ending leaves on screen. Nothing here reaches React. + +/** + * Every end one compare attempt can reach, its failures included. The attempt returns its outcome + * instead of writing it, so the screen is written in exactly one place: past the owner's `commit`. + */ +export type BranchCompareOutcome = + | { readonly kind: 'ready'; readonly result: MobileGitBranchCompareReply } + | { readonly kind: 'unavailable' } + | { readonly kind: 'failed'; readonly message: string } + +/** + * Total by construction: a throw here would reach a caller that only ever voids this load. Null is + * the superseded answer, which the owner reads as no value at all. + */ +export async function readBranchCompareOutcome( + client: RpcOperationSender, + worktreeId: string, + currency: RequestCurrency +): Promise { + try { + const baseRef = await resolveMobileBranchCompareBaseRef(client, worktreeId) + // Resolving the base ref is itself a round trip, so the scope may have moved while it was out. + // Stopping here is what keeps a superseded attempt's compare off the wire: refusing it at commit + // would be just as safe on screen but would have sent the request. + if (!currency.isCurrent()) { + return null + } + if (!baseRef) { + return { kind: 'failed', message: 'Unable to resolve the base branch for comparison.' } + } + const reply = await gitBranchCompareRead.request(client, { + worktree: `id:${worktreeId}`, + baseRef + }) + // Why the raw refusal: a host that does not offer git to mobile is a capability gap this + // screen degrades on, and no acceptance policy carries the code and message through. + if (isMobileGitUnavailableReply(reply)) { + return { kind: 'unavailable' } + } + try { + return { kind: 'ready', result: gitBranchCompareRead.interpret(reply) } + } catch (error) { + return { + kind: 'failed', + message: refusedRpcMessageOrFallback(error, 'Unable to load committed changes') + } + } + } catch (err) { + return { + kind: 'failed', + message: err instanceof Error ? err.message : 'Unable to load committed changes' + } + } +} + +/** What an outcome leaves on screen, given what this caller wants kept when the attempt fails. */ +export function nextBranchCompareState( + outcome: BranchCompareOutcome, + previous: MobileBranchCompareState, + preserveReadyOnFailure: boolean +): MobileBranchCompareState { + if (outcome.kind === 'ready') { + return { kind: 'ready', result: outcome.result } + } + // Why: wiping a prior ready compare to idle makes Changes say "No Changes" even when commits + // still exist (e.g. after abort-merge refresh). + if (preserveReadyOnFailure && previous.kind === 'ready') { + return previous + } + return outcome.kind === 'unavailable' + ? { kind: 'idle' } + : { kind: 'error', message: outcome.message } +} diff --git a/mobile/src/source-control/mobile-pr-chip-summary.test.ts b/mobile/src/source-control/mobile-pr-chip-summary.test.ts index 36f04eb832c..70d05d669e0 100644 --- a/mobile/src/source-control/mobile-pr-chip-summary.test.ts +++ b/mobile/src/source-control/mobile-pr-chip-summary.test.ts @@ -26,7 +26,7 @@ function check( } function ready(prInfo: PRInfo, checks: PRCheckDetail[]): PrSidebarState { - return { kind: 'ready', data: { pr: prInfo, checks, details: null } } + return { kind: 'ready', data: { pr: prInfo, checks, details: null, checksError: null } } } describe('buildMobilePrChipSummary', () => { diff --git a/mobile/src/source-control/mobile-source-control-primary-action.test.ts b/mobile/src/source-control/mobile-source-control-primary-action.test.ts index 8314ecb2d80..e62ab093eb2 100644 --- a/mobile/src/source-control/mobile-source-control-primary-action.test.ts +++ b/mobile/src/source-control/mobile-source-control-primary-action.test.ts @@ -108,6 +108,7 @@ describe('buildMobileSourceControlPrimaryAction', () => { entries: [], summary: { status: 'ready', + errorMessage: undefined, baseRef: 'main', baseOid: 'base', compareRef: 'HEAD', diff --git a/mobile/src/source-control/use-mobile-source-control-loaders.test.ts b/mobile/src/source-control/use-mobile-source-control-loaders.test.ts new file mode 100644 index 00000000000..32f24de1b7a --- /dev/null +++ b/mobile/src/source-control/use-mobile-source-control-loaders.test.ts @@ -0,0 +1,264 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { View } from 'react-native' +import type { RpcClient } from '../transport/rpc-client' +import type { RpcResponse } from '../transport/types' +import { useMobileSourceControlLoaders } from './use-mobile-source-control-loaders' + +// The screen-state module these loaders share with the panel pulls the icon set in; none of it is +// reachable from a hook that renders nothing. +vi.mock('lucide-react-native', () => ({ + ArrowDown: vi.fn(), + ArrowDownUp: vi.fn(), + ArrowUp: vi.fn(), + Check: vi.fn(), + CloudUpload: vi.fn(), + GitBranch: vi.fn(), + GitPullRequestArrow: vi.fn(), + History: vi.fn(), + RefreshCw: vi.fn() +})) + +/** + * What the branch-compare leg now rests on and nothing else held: the owner's commit verdict is the + * only thing that decides which of two overlapping compares reaches the screen, the render-phase + * `reset()` is the only thing that retires a compare when the route identity moves, and the owner's + * currency probe is the only thing that keeps a superseded attempt off the wire. All three are + * written as explicit settlement orders rather than timers, so each case states its schedule. + */ + +const WORKTREE = 'repo42::/p' +const IDENTITY = `host-1:${WORKTREE}` + +type PendingCall = { method: string; params: unknown; settle: (reply: RpcResponse) => void } + +function success(result: unknown): RpcResponse { + return { id: 'call', ok: true, result, _meta: { runtimeId: 'runtime-1' } } +} + +const STATUS_REPLY = success({ + branch: 'feature', + head: 'head-oid', + entries: [{ path: 'src/app.ts', status: 'modified', area: 'unstaged', added: 4, removed: 1 }], + upstreamStatus: { hasUpstream: true, ahead: 1, behind: 0, upstreamName: 'origin/feature' } +}) + +const REPO_LIST_REPLY = success({ repos: [] }) + +function worktreeReply(baseRef: string): RpcResponse { + return success({ worktree: { baseRef } }) +} + +function compareReply(baseRef: string): RpcResponse { + return success({ + summary: { + baseRef, + baseOid: 'base-oid', + compareRef: 'feature', + headOid: 'head-oid', + mergeBase: 'merge-base', + changedFiles: 1, + status: 'ready' + }, + entries: [{ path: 'src/app.ts', status: 'modified', added: 2, removed: 1 }] + }) +} + +function fakeClient(calls: PendingCall[]): RpcClient { + const sendRequest = (method: string, params?: unknown): Promise => + new Promise((resolve) => { + calls.push({ method, params, settle: resolve }) + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these loaders reach `sendRequest` and nothing else on the client; the owner's scope holds the instance by identity without calling it. + return { sendRequest } as RpcClient +} + +/** Settles the oldest unanswered call for `method`, which is what orders one attempt against another. */ +async function settleOldest( + calls: PendingCall[], + method: string, + reply: RpcResponse +): Promise { + const index = calls.findIndex((call) => call.method === method) + if (index === -1) { + throw new Error(`The schedule expected a pending ${method}`) + } + const [call] = calls.splice(index, 1) + await act(async () => call.settle(reply)) +} + +/** Settles the compare an attempt sent, named by the base ref that attempt resolved. */ +async function settleCompare(calls: PendingCall[], baseRef: string): Promise { + const index = calls.findIndex( + (call) => + call.method === 'git.branchCompare' && + typeof call.params === 'object' && + call.params !== null && + 'baseRef' in call.params && + call.params.baseRef === baseRef + ) + if (index === -1) { + throw new Error(`The schedule expected a pending compare against ${baseRef}`) + } + const [call] = calls.splice(index, 1) + await act(async () => call.settle(compareReply(baseRef))) +} + +/** Resolves one whole base-ref lookup: the worktree summary and the repo list go out together. */ +async function settleBaseRefLookup(calls: PendingCall[], baseRef: string): Promise { + await settleOldest(calls, 'worktree.show', worktreeReply(baseRef)) + await settleOldest(calls, 'repo.list', REPO_LIST_REPLY) +} + +function pendingCount(calls: PendingCall[], method: string): number { + return calls.filter((call) => call.method === method).length +} + +/** Stable across renders: the mount effect keys on the callbacks it was handed, so a fresh closure + * per render would re-send the status load and hide the schedule these cases are written in. */ +const IGNORE_ACTION_ERROR = (): void => {} + +// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `setRootRef` compares the node against null and returns; it reads no member of it. +const ROOT_NODE = {} as View + +describe('useMobileSourceControlLoaders branch compare', () => { + let renderer: ReactTestRenderer | null = null + + afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + }) + + type Loaders = ReturnType + + async function mount( + client: RpcClient, + read: { loaders: Loaders | null } + ): Promise<(identityKey: string, worktreeId: string) => Promise> { + function Harness(props: { identityKey: string; worktreeId: string }): null { + read.loaders = useMobileSourceControlLoaders({ + client, + connState: 'connected', + statusIdentityKey: props.identityKey, + worktreeId: props.worktreeId, + setActionError: IGNORE_ACTION_ERROR + }) + return null + } + await act(async () => { + renderer = create(createElement(Harness, { identityKey: IDENTITY, worktreeId: WORKTREE })) + }) + return async (identityKey, worktreeId) => { + await act(async () => { + renderer?.update(createElement(Harness, { identityKey, worktreeId })) + }) + } + } + + it('lets the newest compare publish and refuses the one it superseded', async () => { + const calls: PendingCall[] = [] + const read: { loaders: Loaders | null } = { loaders: null } + await mount(fakeClient(calls), read) + + // First attempt, taken as far as a compare on the wire. + await settleOldest(calls, 'git.status', STATUS_REPLY) + await settleBaseRefLookup(calls, 'origin/dev') + expect(pendingCount(calls, 'git.branchCompare')).toBe(1) + + // Second attempt, started while the first compare is still out. A forced status load is how the + // screen reaches a second compare: the unforced one would join the in-flight status instead. + await act(async () => { + void read.loaders?.loadStatus({ force: true }) + }) + await settleOldest(calls, 'git.status', STATUS_REPLY) + await settleBaseRefLookup(calls, 'origin/main') + // Two physical compares, not one: an attempt never shares its predecessor's reply. + expect(pendingCount(calls, 'git.branchCompare')).toBe(2) + + await settleCompare(calls, 'origin/main') + expect(read.loaders?.branchCompareState).toEqual({ + kind: 'ready', + result: expect.objectContaining({ + summary: expect.objectContaining({ baseRef: 'origin/main' }) + }) + }) + + // The superseded reply settles last and has nowhere to land. + await settleCompare(calls, 'origin/dev') + expect(read.loaders?.branchCompareState).toEqual({ + kind: 'ready', + result: expect.objectContaining({ + summary: expect.objectContaining({ baseRef: 'origin/main' }) + }) + }) + }) + + it('sends no compare for an attempt superseded while it resolved its base ref', async () => { + const calls: PendingCall[] = [] + const read: { loaders: Loaders | null } = { loaders: null } + await mount(fakeClient(calls), read) + + // First attempt, stopped mid base-ref lookup: nothing of it has reached `git.branchCompare` yet. + await settleOldest(calls, 'git.status', STATUS_REPLY) + expect(pendingCount(calls, 'worktree.show')).toBe(1) + + // Second attempt supersedes it while that lookup is still out. + await act(async () => { + void read.loaders?.loadStatus({ force: true }) + }) + await settleOldest(calls, 'git.status', STATUS_REPLY) + + // The superseded attempt resumes with its base ref in hand and stops on the probe. No compare + // has been settled in this case, so what is pending is everything that was ever sent. + await settleBaseRefLookup(calls, 'origin/dev') + expect(pendingCount(calls, 'git.branchCompare')).toBe(0) + + // Exactly one compare on the wire, the live attempt's, which is the request count main sent. + await settleBaseRefLookup(calls, 'origin/main') + expect(pendingCount(calls, 'git.branchCompare')).toBe(1) + + await settleCompare(calls, 'origin/main') + expect(read.loaders?.branchCompareState).toEqual({ + kind: 'ready', + result: expect.objectContaining({ + summary: expect.objectContaining({ baseRef: 'origin/main' }) + }) + }) + }) + + it('sends no compare for an attempt the route detached under', async () => { + const calls: PendingCall[] = [] + const read: { loaders: Loaders | null } = { loaders: null } + await mount(fakeClient(calls), read) + await act(async () => read.loaders?.setRootRef(ROOT_NODE)) + + await settleOldest(calls, 'git.status', STATUS_REPLY) + expect(pendingCount(calls, 'worktree.show')).toBe(1) + + // The route detaches mid base-ref lookup. Dropping the mount latch is not enough on its own: + // only the detach's `reset()` retires the attempt, and the probe is what reads that. + await act(async () => read.loaders?.setRootRef(null)) + + await settleBaseRefLookup(calls, 'origin/dev') + expect(pendingCount(calls, 'git.branchCompare')).toBe(0) + }) + + it('refuses a compare whose route identity moved while it was out', async () => { + const calls: PendingCall[] = [] + const read: { loaders: Loaders | null } = { loaders: null } + const rerender = await mount(fakeClient(calls), read) + + await settleOldest(calls, 'git.status', STATUS_REPLY) + await settleBaseRefLookup(calls, 'origin/dev') + expect(pendingCount(calls, 'git.branchCompare')).toBe(1) + + // The route is reused for another worktree. Its status load is still out, so no compare has + // entered the new scope yet: this is the window the identity check used to own. + await rerender('host-1:repo42::/other', 'repo42::/other') + expect(read.loaders?.branchCompareState).toEqual({ kind: 'idle' }) + + await settleCompare(calls, 'origin/dev') + expect(read.loaders?.branchCompareState).toEqual({ kind: 'idle' }) + }) +}) diff --git a/mobile/src/source-control/use-mobile-source-control-loaders.ts b/mobile/src/source-control/use-mobile-source-control-loaders.ts index 8ab4cbc0e2a..6856bc2a820 100644 --- a/mobile/src/source-control/use-mobile-source-control-loaders.ts +++ b/mobile/src/source-control/use-mobile-source-control-loaders.ts @@ -1,16 +1,22 @@ import { useCallback, useEffect, useRef, useState, type MutableRefObject } from 'react' import { View } from 'react-native' import type { RpcClient } from '../transport/rpc-client' -import { refusedRpcMessageOrFallback } from '../transport/rpc-refusal-message' +import { + GenerationScopedRequestOwner, + type RequestScope +} from '../transport/generation-scoped-request-owner' import type { ConnectionState } from '../transport/types' -import { resolveMobileBranchCompareBaseRef } from './mobile-branch-base-ref' -import { gitBranchCompareRead, gitStatusHostPayloadRead } from './mobile-git-read-operations' +import { + nextBranchCompareState, + readBranchCompareOutcome, + type BranchCompareOutcome +} from './mobile-branch-compare-outcome' +import { gitStatusHostPayloadRead } from './mobile-git-read-operations' import { isMobileGitTransientRefreshError, isMobileGitUnavailableReply, readMobileGitRefusal } from './mobile-git-status' -import type { MobileGitBranchCompareReply } from './git-compare-reply-schema' import { SELECTOR_RETRY_COUNT, SELECTOR_RETRY_DELAY_MS, @@ -30,6 +36,10 @@ type Params = { onStatusLoadSuccess?: () => void } +/** The compare is the whole worktree against its base, so its request carries no further parameters. */ +type BranchCompareParameters = Readonly> +const WHOLE_WORKTREE: BranchCompareParameters = {} + export type MobileSourceControlLoaders = { screenState: ScreenState setScreenState: (next: ScreenState | ((prev: ScreenState) => ScreenState)) => void @@ -52,11 +62,12 @@ export function useMobileSourceControlLoaders(params: Params): MobileSourceContr kind: 'idle' }) const currentStatusIdentityRef = useRef('') - const currentBranchCompareIdentityRef = useRef('') const loadGenerationRef = useRef(0) - const branchCompareGenerationRef = useRef(0) const mountedRef = useRef(true) const statusLoadInFlightRef = useRef(null) + const branchCompare = useRef( + new GenerationScopedRequestOwner() + ).current // Why: the same route can be reused for another worktree/host (identity change); // a kept-on-failure `ready` state would otherwise show the previous worktree's // data until the fresh load resolves. Reset to loading in the render phase (the @@ -64,102 +75,65 @@ export function useMobileSourceControlLoaders(params: Params): MobileSourceContr const lastResetIdentityRef = useRef(statusIdentityKey) if (lastResetIdentityRef.current !== statusIdentityKey) { lastResetIdentityRef.current = statusIdentityKey + // Retire here rather than leaving it to the next load's scope: that load only starts once the + // fresh status returns, and an in-flight compare would publish the old worktree's commits first. + branchCompare.reset() setScreenState({ kind: 'loading' }) setBranchCompareState({ kind: 'idle' }) } currentStatusIdentityRef.current = statusIdentityKey - currentBranchCompareIdentityRef.current = statusIdentityKey - const setRootRef = useCallback((node: View | null): void => { - if (node !== null) { - mountedRef.current = true - return - } - // Why: source-control RPC loads can outlive the route; invalidate pending - // writes when the screen detaches without a passive cleanup-only Effect. - mountedRef.current = false - loadGenerationRef.current += 1 - branchCompareGenerationRef.current += 1 - }, []) + const setRootRef = useCallback( + (node: View | null): void => { + if (node !== null) { + mountedRef.current = true + return + } + // Why: source-control RPC loads can outlive the route; invalidate pending + // writes when the screen detaches without a passive cleanup-only Effect. + mountedRef.current = false + loadGenerationRef.current += 1 + branchCompare.reset() + }, + [branchCompare] + ) const loadBranchCompare = useCallback( async (options?: { preserveReadyOnFailure?: boolean }) => { - const loadKey = statusIdentityKey - const generation = branchCompareGenerationRef.current + 1 - branchCompareGenerationRef.current = generation - const isCurrentLoad = () => - mountedRef.current && - branchCompareGenerationRef.current === generation && - currentBranchCompareIdentityRef.current === loadKey - + // A compare is a refresh, so nothing it holds is reusable and no attempt may share another's + // reply: retiring first is what makes the newest attempt the only one that can still publish. + branchCompare.reset() if (!worktreeId || !client || connState !== 'connected') { - if (isCurrentLoad()) { + if (mountedRef.current) { setBranchCompareState({ kind: 'idle' }) } return false } + // What retires a compare: this host and this route identity, which is `${hostId}\0${worktreeId}` + // and so carries the workspace already. It is in the scope as well as in the render-phase + // retire, so a scope the owner has not seen still retires on its own if a load ever reaches it + // before that block does. + const scope: RequestScope = [client, statusIdentityKey] setBranchCompareState((prev) => (prev.kind === 'ready' ? prev : { kind: 'loading' })) - try { - const baseRef = await resolveMobileBranchCompareBaseRef(client, worktreeId) - if (!isCurrentLoad()) { - return false - } - if (!baseRef) { - // Why: wiping a prior ready compare to idle makes Changes say "No - // Changes" even when commits still exist (e.g. after abort-merge refresh). - setBranchCompareState((prev) => { - if (options?.preserveReadyOnFailure && prev.kind === 'ready') { - return prev - } - return { - kind: 'error', - message: 'Unable to resolve the base branch for comparison.' - } - }) - return false - } - const reply = await gitBranchCompareRead.request(client, { - worktree: `id:${worktreeId}`, - baseRef - }) - if (!isCurrentLoad()) { - return false - } - // Why the raw refusal: a host that does not offer git to mobile is a capability gap this - // screen degrades on, and no acceptance policy carries the code and message through. - if (isMobileGitUnavailableReply(reply)) { - setBranchCompareState((prev) => { - if (options?.preserveReadyOnFailure && prev.kind === 'ready') { - return prev - } - return { kind: 'idle' } - }) - return false - } - let compared: MobileGitBranchCompareReply - try { - compared = gitBranchCompareRead.interpret(reply) - } catch (error) { - throw new Error(refusedRpcMessageOrFallback(error, 'Unable to load committed changes')) - } - setBranchCompareState({ kind: 'ready', result: compared }) - return true - } catch (err) { - if (!isCurrentLoad()) { - return false - } - const message = err instanceof Error ? err.message : 'Unable to load committed changes' - setBranchCompareState((prev) => { - if (options?.preserveReadyOnFailure && prev.kind === 'ready') { - return prev - } - return { kind: 'error', message } - }) + const loaded = await branchCompare.load(scope, WHOLE_WORKTREE, (currency) => + readBranchCompareOutcome(client, worktreeId, currency) + ) + // The mount latch is not the owner's to keep: a detached route has no screen to publish to, + // which is a fact about the view, not about which reply is current. + if (!loaded || !mountedRef.current) { return false } + if (branchCompare.commit(loaded.lease, loaded.value) !== 'committed') { + return false + } + const outcome = loaded.value + setBranchCompareState((prev) => + nextBranchCompareState(outcome, prev, options?.preserveReadyOnFailure === true) + ) + return outcome.kind === 'ready' }, - [client, connState, statusIdentityKey, worktreeId] + [branchCompare, client, connState, statusIdentityKey, worktreeId] ) const loadStatus = useCallback( diff --git a/mobile/src/tasks/task-project-board-reply-schema.test.ts b/mobile/src/tasks/task-project-board-reply-schema.test.ts index 841dcb9d741..c36cc9da6f1 100644 --- a/mobile/src/tasks/task-project-board-reply-schema.test.ts +++ b/mobile/src/tasks/task-project-board-reply-schema.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { GitHubProjectOwnerType } from '../../../src/shared/github/project-types' import { + PROJECT_OWNER_TYPE, taskProjectAccessibleListSchema, taskProjectAssignableUserListSchema, taskProjectCommentMutationSchema, @@ -61,13 +61,9 @@ describe('project envelopes', () => { describe('ownerType is a closed enum', () => { it('takes both arms the host validates', () => { - // Keyed by the host's own type, so an arm added to GitHubProjectOwnerType fails tsc here - // rather than silently dropping every row that carries it. - const HOST_OWNER_TYPES: Record = { - organization: true, - user: true - } - for (const ownerType of Object.keys(HOST_OWNER_TYPES)) { + // The arm list is pinned to GitHubProjectOwnerType in the schema module, where tsc looks; this + // loop proves every pinned arm parses rather than dropping the row that carries it. + for (const ownerType of PROJECT_OWNER_TYPE) { const parsed = taskProjectRefSchema.safeParse({ ok: true, owner: 'o', ownerType, number: 3 }) expect(parsed.success && parsed.data).toMatchObject({ ownerType }) } diff --git a/mobile/src/tasks/task-project-board-reply-schema.ts b/mobile/src/tasks/task-project-board-reply-schema.ts index 7ae763440ab..e308325f61f 100644 --- a/mobile/src/tasks/task-project-board-reply-schema.ts +++ b/mobile/src/tasks/task-project-board-reply-schema.ts @@ -1,5 +1,6 @@ import { z } from 'zod' -import { salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage' +import type { GitHubProjectOwnerType } from '../../../src/shared/github/project-types' +import { hostUnionArms, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage' import { assignableUserListSchema, detailCheckListSchema, @@ -42,7 +43,11 @@ import { * The corpus exercises `'organization'` (`tk-project-board-load`); `'user'` is unexercised but * inside the set, so no reply can drop on it. */ -const PROJECT_OWNER_TYPE = ['organization', 'user'] as const +// Pinned to the host's own union through hostUnionArms: an arm added or dropped host-side fails tsc. +export const PROJECT_OWNER_TYPE = hostUnionArms({ + organization: true, + user: true +}) const projectMessage = (name: string) => salvagedOptional(name, z.string()) const projectCount = (name: string) => salvagedOptional(name, z.number()) diff --git a/mobile/src/tasks/task-provider-entity-reply-schema.test.ts b/mobile/src/tasks/task-provider-entity-reply-schema.test.ts index d2af64a1a3e..2e864b17372 100644 --- a/mobile/src/tasks/task-provider-entity-reply-schema.test.ts +++ b/mobile/src/tasks/task-provider-entity-reply-schema.test.ts @@ -3,6 +3,7 @@ import type { z } from 'zod' import { assignableUserListSchema, detailCheckListSchema, + DETAIL_FILE_STATUS, detailCommentListSchema, detailFileListSchema, reviewSummaryListSchema, @@ -154,15 +155,8 @@ describe('a check row and a file row', () => { }) it('carries every file status the host will accept back and drops one it would refuse', () => { - for (const status of [ - 'added', - 'modified', - 'removed', - 'renamed', - 'copied', - 'changed', - 'unchanged' - ]) { + // The arm list is pinned to GitHubPRFile['status'] in the schema module, where tsc looks. + for (const status of DETAIL_FILE_STATUS) { expect(reads(detailFileListSchema, [{ path: 'a', status }])[0]?.status).toBe(status) } // Absent is what the call site turns into `'modified'`; the host's own params enum would diff --git a/mobile/src/tasks/task-provider-entity-reply-schema.ts b/mobile/src/tasks/task-provider-entity-reply-schema.ts index 45678712bef..da243ac616c 100644 --- a/mobile/src/tasks/task-provider-entity-reply-schema.ts +++ b/mobile/src/tasks/task-provider-entity-reply-schema.ts @@ -1,5 +1,6 @@ import { z } from 'zod' -import { salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage' +import type { GitHubPRFile } from '../../../src/shared/github/pull-request-types' +import { hostUnionArms, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage' import { prCount, prFlag, prNullableText, prText } from '../session/github-pr-entity-reply-schema' // The entities the tasks screen's provider replies are built out of: the mutation envelope every @@ -22,15 +23,16 @@ import { prCount, prFlag, prNullableText, prText } from '../session/github-pr-en // plain salvaged-member combinators over zod-salvage, and one definition is what keeps "absent // stays absent, malformed reads as absent" identical on both surfaces. -const DETAIL_FILE_STATUS = [ - 'added', - 'modified', - 'removed', - 'renamed', - 'copied', - 'changed', - 'unchanged' -] as const +// Pinned to the host's own union through hostUnionArms: an arm added or dropped host-side fails tsc. +export const DETAIL_FILE_STATUS = hostUnionArms({ + added: true, + modified: true, + removed: true, + renamed: true, + copied: true, + changed: true, + unchanged: true +}) /** * One conversation comment, as every task sheet holds it. diff --git a/mobile/src/tasks/workspace-source-reply-schema.test.ts b/mobile/src/tasks/workspace-source-reply-schema.test.ts index f4c7237d951..30597e8e536 100644 --- a/mobile/src/tasks/workspace-source-reply-schema.test.ts +++ b/mobile/src/tasks/workspace-source-reply-schema.test.ts @@ -7,6 +7,7 @@ import { repoSetupHooksSchema, repoSparsePresetListSchema, repoSparsePresetSaveSchema, + SSH_CONNECTION_STATUS, sshConnectionStateSchema } from './workspace-source-reply-schema' @@ -57,22 +58,10 @@ describe('the SSH connection record', () => { }) describe('status is an open enum that degrades to disconnected', () => { - // Keyed by SshConnectionStatus so the compiler, not this list, decides what "every arm" means: - // an arm added to or removed from the host union fails tsc here before any test runs. That is - // the check the schema's own arm list cannot make about itself. - const HOST_ARMS: Record = { - disconnected: true, - connecting: true, - 'auth-failed': true, - 'deploying-relay': true, - connected: true, - reconnecting: true, - 'reconnection-failed': true, - error: true - } - + // The arm list is pinned to SshConnectionStatus in the schema module, where tsc looks; this loop + // proves every pinned arm survives the parse rather than degrading. it('takes every arm the host declares, so nothing it sends today degrades', () => { - for (const status of Object.keys(HOST_ARMS)) { + for (const status of SSH_CONNECTION_STATUS) { const parsed = sshConnectionStateSchema.safeParse({ state: { ...connected, status } }) expect(parsed.success && parsed.data).toMatchObject({ status }) } diff --git a/mobile/src/tasks/workspace-source-reply-schema.ts b/mobile/src/tasks/workspace-source-reply-schema.ts index 93e80eb0f64..54b0174f65f 100644 --- a/mobile/src/tasks/workspace-source-reply-schema.ts +++ b/mobile/src/tasks/workspace-source-reply-schema.ts @@ -1,5 +1,11 @@ import { z } from 'zod' -import { openEnum, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage' +import type { SshConnectionStatus } from '../../../src/shared/ssh-types' +import { + hostUnionArms, + openEnum, + salvagedOptional, + salvagingArray +} from '../../../src/shared/zod-salvage' // The repo and SSH reads the workspace-create drawer runs. Checked against // src/main/runtime/rpc/methods/ssh.ts:30-46 (getPublicSshState, SshConnectionState in @@ -7,16 +13,17 @@ import { openEnum, salvagedOptional, salvagingArray } from '../../../src/shared/ // answer a bare `string[]`), and repo.ts:87-103/:184-192 (the sparse preset envelopes, the ref // search and the orca.yaml hooks). -const SSH_CONNECTION_STATUS = [ - 'disconnected', - 'connecting', - 'auth-failed', - 'deploying-relay', - 'connected', - 'reconnecting', - 'reconnection-failed', - 'error' -] as const +// Pinned to the host's own union through hostUnionArms: an arm added or dropped host-side fails tsc. +export const SSH_CONNECTION_STATUS = hostUnionArms({ + disconnected: true, + connecting: true, + 'auth-failed': true, + 'deploying-relay': true, + connected: true, + reconnecting: true, + 'reconnection-failed': true, + error: true +}) const sourceText = (name: string) => salvagedOptional(name, z.string()) diff --git a/mobile/src/terminal/use-buffered-terminal-drafts.test.tsx b/mobile/src/terminal/use-buffered-terminal-drafts.test.tsx index 02ae3ddd206..f41e71ed3e9 100644 --- a/mobile/src/terminal/use-buffered-terminal-drafts.test.tsx +++ b/mobile/src/terminal/use-buffered-terminal-drafts.test.tsx @@ -118,7 +118,7 @@ describe('useBufferedTerminalDrafts', () => { renderer = create(createElement(Probe, { activeHandle: 'terminal-old' })) }) act(() => hook().setInput('rejected command')) - let send: ReturnType + let send!: ReturnType act(() => { send = hook().beginBufferedTerminalDraftSend('terminal-old', hook().input) hook().reconcileTerminalTabs( diff --git a/mobile/src/test-support/rpc-recording/README.md b/mobile/src/test-support/rpc-recording/README.md index 903946eb468..8f8bbc89fc6 100644 --- a/mobile/src/test-support/rpc-recording/README.md +++ b/mobile/src/test-support/rpc-recording/README.md @@ -183,7 +183,7 @@ off the context `client-context.tsx` keeps module-private, and each used to carr `exports.recorderHostClientContext = Ctx;`. That string names a local no type checker follows, so five spellings were five independent ways to reach a `ReferenceError` seconds into a recording. `hostClientContextExposure` is the one copy; the trade is that it sits inside `recorderSha256`, so -editing it re-records all 727 goldens rather than the five families. A rename of the local is still +editing it re-records all 787 goldens rather than the five families. A rename of the local is still invisible to `tsc` — nothing short of editing the product module makes a private local checkable — so `adapter-seam.test.ts` asserts the declaration it names exists exactly once, and refuses a sixth inline copy. @@ -378,8 +378,11 @@ families because no reference states are defined for them. ## What this oracle does and does not see -It replays 368 manifest scenarios against frozen goldens and fails on any divergence: 727 goldens -over 888 tests, all inside `pnpm --dir mobile test`. Counts quoted further down are measurements of +It replays 397 manifest scenarios against frozen goldens and fails on any divergence: 787 goldens +over 790 tests, all inside `pnpm --dir mobile test`. Counted with +`python3 -c "import json;print(len(json.load(open('mobile/rpc-foundation/pilot-scenarios.json'))['scenarios']))"`, +`find mobile/rpc-foundation/goldens -type f | wc -l`, and the reported total of +`vitest run src/test-support/rpc-recording/{pilot,family}-recordings.test.ts src/test-support/rpc-recording/derived-goldens.test.ts`. Counts quoted further down are measurements of the change they describe and are not restatements of this one. For a migration it answers one question — does the rewritten call site produce the same sender calls, settlements, state and effects as main did? @@ -610,10 +613,10 @@ the drop happened under, and records a non-empty report as a `reply-salvage` eff operation, the method, the decoded variant, the dropped paths and the count. Nothing in the product tree changes: the report was already being built and thrown away. -No golden carries one. All 19,384 checked reads in the corpus decode their reply whole, on every -reply partition — the matrix varies the envelope a host sends, not the shape of a row inside a -result — so this observation pins the absence rather than a recorded drop. What it buys is the -next tightening: an element or member schema narrowed so a recorded row stops parsing moves the +44 of the 787 goldens carry one, and every other checked read in the corpus decodes its reply +whole (`grep -l reply-salvage mobile/rpc-foundation/goldens/*.json | wc -l`). The matrix varies the +envelope a host sends rather than the shape of a row inside a result, so on most families this +observation pins an absence rather than a recorded drop. What it buys is the next tightening: an element or member schema narrowed so a recorded row stops parsing moves the golden even where nothing downstream reads the row. `salvage-observation.test.ts` is what keeps the observation itself honest, driving a malformed row and a malformed optional through the real `git.status` reply schema, because a refactor that stopped reporting would otherwise leave every diff --git a/mobile/src/test-support/rpc-recording/adapters/mobile-web-bundle-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/mobile-web-bundle-mount-adapters.ts new file mode 100644 index 00000000000..af2f00b5e3e --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/mobile-web-bundle-mount-adapters.ts @@ -0,0 +1,101 @@ +import type { MountAdapter, MountContext } from '../recording-scenario' +import type { operationModuleLoader } from '../operation-module-loader' + +const OPERATIONS_MODULE = 'mobile/src/transport/mobile-web-bundle-operations.ts' +const FETCH_MODULE = 'mobile/src/transport/mobile-web-bundle-fetch.ts' +const RPC_OPERATION_MODULE = 'mobile/src/transport/rpc-operation.ts' + +type OperationsModule = typeof import('../../../transport/mobile-web-bundle-operations') +type FetchModule = typeof import('../../../transport/mobile-web-bundle-fetch') +type RpcOperationModule = typeof import('../../../transport/rpc-operation') + +/** The host's own code where it has one, so the projection observes the code reader too. */ +function describeFailure(operations: OperationsModule, error: unknown): string { + const code = operations.readMobileWebBundleErrorCode(error) + if (code !== null) { + return `refused: ${code}` + } + return `failed: ${error instanceof Error ? error.message : String(error)}` +} + +/** + * The two client reads that fetch the desktop-served mobile web bundle. + * + * `bundle-manifest` drives the manifest descriptor alone, so the loose reader's verdict on one reply + * is the whole observation. `bundle-fetch` drives the paging flow, and its state carries the decoded + * bytes of every asset rather than a count: a reassembly that misplaces a chunk still has the right + * length, and only the bytes say so. + */ +export function mobileWebBundleMountAdapters( + modules: ReturnType +): Record { + return { + 'mobileWeb.bundle-manifest': ({ client }: MountContext) => { + const operations = modules.load(OPERATIONS_MODULE) + const runRpcOperation = modules.load(RPC_OPERATION_MODULE).runRpcOperation + let outcome: unknown = 'unread' + return { + action() { + const started = runRpcOperation(client, operations.mobileWebBundleManifestRead, null) + started.then( + (reply) => { + outcome = { + chunkBytes: reply.chunkBytes, + buildId: reply.manifest.buildId, + entrypoint: reply.manifest.entrypoint, + paths: reply.manifest.assets.map((asset) => asset.path) + } + }, + (error: unknown) => { + outcome = describeFailure(operations, error) + } + ) + return started + }, + state: () => ({ outcome }), + dispose: () => {} + } + }, + 'mobileWeb.bundle-fetch': ({ client, effect }: MountContext) => { + const operations = modules.load(OPERATIONS_MODULE) + const fetchMobileWebBundle = modules.load(FETCH_MODULE).fetchMobileWebBundle + let outcome: unknown = 'unfetched' + let assets: unknown = null + return { + action() { + // Projected rather than returned whole: the result carries a Map of Uint8Arrays, and the + // observation refuses a non-plain object, which loses the settlement and files an + // unhandled rejection in its place. + return fetchMobileWebBundle({ + client, + onProgress: (progress) => { + effect('bundle-progress', { + completedAssets: progress.completedAssets, + totalAssets: progress.totalAssets, + receivedBytes: progress.receivedBytes + }) + } + }).then( + (fetched) => { + outcome = { + buildId: fetched.manifest.buildId, + assetCount: fetched.assets.size, + totalBytes: fetched.totalBytes + } + assets = Object.fromEntries( + [...fetched.assets].map(([path, bytes]) => [path, new TextDecoder().decode(bytes)]) + ) + return outcome + }, + (error: unknown) => { + outcome = describeFailure(operations, error) + throw error + } + ) + }, + state: () => ({ outcome, assets }), + dispose: () => {} + } + } + } +} diff --git a/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts b/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts index 615036e40c3..b706040a280 100644 --- a/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts +++ b/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts @@ -27,6 +27,7 @@ import { homeAccountsMountAdapters } from './home-accounts-mount-adapters' import { hostScreenMountAdapters } from './host-screen-mount-adapters' import { hostWorktreeActionMountAdapters } from './host-worktree-action-mount-adapters' import { hostedReviewMountAdapters } from './hosted-review-mount-adapters' +import { mobileWebBundleMountAdapters } from './mobile-web-bundle-mount-adapters' import { nativeChatPagingMountAdapters } from './native-chat-paging-mount-adapters' import { nativeChatWriteMountAdapters } from './native-chat-write-mount-adapters' import { newTabAgentMountAdapters } from './new-tab-agent-mount-adapters' @@ -129,6 +130,7 @@ export const MOUNTED_OPERATION_MODULES: readonly MountedOperationModule[] = [ mounts: hostWorktreeActionMountAdapters }, { source: 'hosted-review-mount-adapters.ts', mounts: hostedReviewMountAdapters }, + { source: 'mobile-web-bundle-mount-adapters.ts', mounts: mobileWebBundleMountAdapters }, { source: 'native-chat-paging-mount-adapters.ts', mounts: nativeChatPagingMountAdapters }, { source: 'native-chat-write-mount-adapters.ts', mounts: nativeChatWriteMountAdapters }, { source: 'new-tab-agent-mount-adapters.ts', mounts: newTabAgentMountAdapters }, diff --git a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts index 8b2843bed62..58b12257cdf 100644 --- a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts +++ b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts @@ -73,6 +73,14 @@ export const OPERATION_MUTATIONS = { before: 'const snapshot = decodeAccountsSnapshot(accounts.value)', after: 'const snapshot = decodeAccountsSnapshot(reply)' }, + // Writes every chunk of an asset at offset 0, so a multi-chunk asset reassembles as its last + // chunk over a zero-filled buffer. The length still matches the manifest; only the sha256 check + // and the decoded bytes in the projection say the bundle is wrong. + 'mobile-web-bundle-chunk-placement': { + file: 'mobile-web-bundle-fetch.ts', + before: 'whole.set(bytes, offset)', + after: 'whole.set(bytes, 0)' + }, // Puts the workspace catalog's reply back behind an unchecked reader, so a reply carrying neither // rows nor an `unchanged` token reaches `admitWorktreeCatalogResponse` as an invalid admission // instead of being named at the boundary — main's answer, and the one the host screen showed as @@ -127,11 +135,19 @@ export const OPERATION_MUTATIONS = { before: 'linearConnected: linear?.connected === true', after: 'linearConnected: linear !== null' }, - // Reads the host platform from the wrong field of the host.platform result. + // Reads the host platform from the wrong field of the host.platform result. Re-anchored where + // step 7 moved the read: the hand-rolled `readHostPlatform` became the reply schema's own + // projection, so the anchor is that projection. The defect it injects — rows labelled with a + // platform the host never reported — is unchanged. 'repo-metadata-platform': { - file: 'use-host-repo-metadata.ts', - before: 'const platform = (result as { platform?: unknown } | null)?.platform', - after: 'const platform = (result as { hostPlatform?: unknown } | null)?.hostPlatform' + file: 'host-screen-reply-schema.ts', + before: ` .looseObject({ platform: salvagedOptional('platform', z.enum(NODE_PLATFORM_NAMES)) }) + .transform((reply) => reply.platform ?? null)`, + after: ` .looseObject({ + platform: salvagedOptional('platform', z.enum(NODE_PLATFORM_NAMES)), + hostPlatform: salvagedOptional('hostPlatform', z.enum(NODE_PLATFORM_NAMES)) + }) + .transform((reply) => reply.hostPlatform ?? null)` }, // Hydrates the runtime task settings from the envelope rather than the accepted value. 'task-hydration-envelope': { diff --git a/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts b/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts index 8af7f8af5ce..bcdcbf46a9f 100644 --- a/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts +++ b/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts @@ -45,7 +45,8 @@ const mutants: Record = { 'terminal-worktree-connection-resolved': 'worktree-connection-first-repo', 'pr-sidebar-checks-refused': 'pr-sidebar-checks-failure-state', 'tk-item-detail-metadata': 'assignable-user-avatar-null-collapse', - 'worktree-catalog-snapshot-unreadable': 'worktree-catalog-unchecked-reader' + 'worktree-catalog-snapshot-unreadable': 'worktree-catalog-unchecked-reader', + 'mobile-web-bundle-fetch-paged': 'mobile-web-bundle-chunk-placement' } /** * The archived tree's visible state, pinned per seed: b1 serves the poisoned empty inventory, b2 diff --git a/mobile/src/test-support/rpc-recording/mutants/reply-schema-mutations.ts b/mobile/src/test-support/rpc-recording/mutants/reply-schema-mutations.ts new file mode 100644 index 00000000000..e9374d1a01d --- /dev/null +++ b/mobile/src/test-support/rpc-recording/mutants/reply-schema-mutations.ts @@ -0,0 +1,89 @@ +import type { OperationMutation } from '../operation-module-loader' + +/** + * Mutant evidence for the checked reply readers, in the same shape as the adapter-family mutations + * beside it and kept apart from them for one reason: a reader mutation is not killed by a pilot + * scenario's visible state. A pilot serves a *good* reply, and a schema that has stopped checking a + * member still reads a good reply exactly as before. What kills these is the malformed partition of + * a matrix golden, the schema's own unit pin, or a consumer pin — so they are applied by hand and + * the gate that caught each is named below rather than being driven by `pilot-mutants.test.ts`. + * + * Nothing on the recording path imports this file, which `mutant-seam.test.ts` holds. + * + * Two of the first three survived their first run, and both survivals were defects in the gates + * rather than in the readers: + * + * - `file-tab-text-content-optional` survived because the unit pin dropped one required member at + * a time only in prose: it asserted `{ content, truncated }` and `{ content, byteLength }` were + * refused, and each of those is refused by the *other* missing member. The matrix golden masked + * it the same way, because `result-absent` fails on all three at once. The pin now drops exactly + * one member per iteration, and the same pattern was applied to the preview text schema and the + * legacy file list. + * - `ownership-host-id-null-collapse` survived because no golden serves an explicit `null` hostId: + * `files-ownership-local` omits the member instead. A tri-state is a consumer property rather + * than a projection one, so the gate added for it is a consumer pin that captures all three + * states end to end. + */ +export const REPLY_SCHEMA_MUTATIONS = { + /** + * (a) Loosens a member the file tab publishes into its ready document with no guard. + * + * Killed by `src/files/file-tab-doc-reply-schema.test.ts` — "requires the three members the tab + * publishes into a ready document" and "requires the image content buildImageDataUri calls + * replace on", because the anchor appears on both schemas in that file. Not killed by any + * golden: every matrix partition that omits `content` omits its two siblings as well. + */ + 'file-tab-text-content-optional': { + file: 'file-tab-doc-reply-schema.ts', + before: ' content: z.string(),', + after: ' content: z.string().optional(),' + }, + /** + * (b) Puts the directory listing back on the unchecked reader it replaced. + * + * Killed twice. `matrix-files.explorer-screen-files.readdir-1` diverges at + * `files-explorer-legacy-fallback.result-absent:legacy-listed`, field + * `state.elements.Pressable`: the Files tab draws the empty tree and no retry again instead of + * the named error row. `unchecked-rpc-reader-boundary.test.ts` fails in the other direction — + * "has no unlisted file holding an unchecked reader" — because the inventory line for this file + * is gone. + */ + 'file-directory-read-unchecked': { + file: 'mobile-file-explorer-operations.ts', + before: " read: rpcResultVariant('directory-entries', fileDirectoryEntriesSchema)", + after: " read: rpcUncheckedPayloadReader('directory-entries')" + }, + /** + * (c) Collapses the hostId tri-state, which is the one member on this branch where absence and an + * explicit null mean different things: absent is "no host recorded" and captures local, null is a + * host this client cannot place and refuses. The collapse sends a file write to the runtime-local + * host for a workspace whose owner the reply did not establish. + * + * Killed by `src/files/mobile-file-mutation-ownership.test.ts` — "refuses a workspace whose reply + * names an explicit null host" resolves to a local capture instead of rejecting. + */ + 'ownership-host-id-null-collapse': { + file: 'mobile-file-mutation-ownership.ts', + before: ' return buildMobileFileMutationOwnership(summary.hostId, sshState)', + after: ' return buildMobileFileMutationOwnership(summary.hostId ?? undefined, sshState)' + }, + /** + * (d) Puts the closed image-source enum back on the repo icon — the one arm set on this branch + * that was narrower than what the wire can carry. No mobile consumer reads `source`, so the + * enum's only effect is that an icon whose source a later host adds fails the union arm, drops + * whole, and draws the Folder default where main drew the image. + * + * Killed by `src/host-screen/host-screen-reply-schema.test.ts` — "keeps an image icon whose + * source this build has never heard of". No golden kills it, and that is the point: this is the + * member `settings-repo-metadata-icons` was recorded for, and a fixture can only carry a source + * that exists today, so the future-arm case stays a unit property. + */ + 'repo-icon-source-closed': { + file: 'host-screen-reply-schema.ts', + before: ' src: z.string(),', + after: + " src: z.string(),\n source: z.enum(['upload', 'file', 'favicon', 'github'])," + } +} as const satisfies Record> + +export type ReplySchemaMutation = keyof typeof REPLY_SCHEMA_MUTATIONS diff --git a/mobile/src/transport/generation-scoped-lease-compile-fence.ts b/mobile/src/transport/generation-scoped-lease-compile-fence.ts index 039b03f51b8..24e293b73c0 100644 --- a/mobile/src/transport/generation-scoped-lease-compile-fence.ts +++ b/mobile/src/transport/generation-scoped-lease-compile-fence.ts @@ -55,6 +55,17 @@ export function fenceLoaderOnlyReturns(): void { // @ts-expect-error the loader publishes by returning the owner's value, not some other type void paths.load(scope, { query: 'a' }, async () => 'not-a-path-list') void paths.load(scope, { query: 'a' }, async () => null) + void paths.load(scope, { query: 'a' }, async (currency) => (currency.isCurrent() ? ['a'] : null)) +} + +export function fenceCurrencyIsAProbeNotALease(): void { + void paths.load(scope, { query: 'a' }, async (currency) => { + // @ts-expect-error the probe answers currency and is not the lease, so it cannot publish + paths.commit(currency, ['a']) + // @ts-expect-error the probe exposes no generation at all, so there is no member to read + void currency.generation + return null + }) } // @ts-expect-error the lease carries its generation privately; a caller cannot read or compare it diff --git a/mobile/src/transport/generation-scoped-request-owner.ts b/mobile/src/transport/generation-scoped-request-owner.ts index c8655c58a6d..b6b9771fe29 100644 --- a/mobile/src/transport/generation-scoped-request-owner.ts +++ b/mobile/src/transport/generation-scoped-request-owner.ts @@ -37,6 +37,15 @@ export type RequestLease = { readonly [LEASE_VALUE]?: (value: Value) => void } +/** + * Handed to a loader so it can stop before sending a request whose scope has already moved. A probe + * and nothing else: it answers the same question `commit` asks and carries no way to publish, so the + * loader still has only its return value to say anything with. + */ +export type RequestCurrency = { + readonly isCurrent: () => boolean +} + /** Named rather than boolean: a refused publish says which fence refused it. */ type RequestCommitVerdict = 'committed' | 'retired-generation' | 'foreign-owner' @@ -91,12 +100,14 @@ export class GenerationScopedRequestOwner Promise + fn: (currency: RequestCurrency) => Promise ): Promise | null> { const key = this.enter(scope, parameters) return this.inFlight.get(key) ?? this.start(key, fn) @@ -122,16 +133,20 @@ export class GenerationScopedRequestOwner Promise + fn: (currency: RequestCurrency) => Promise ): Promise | null> { - const lease: RequestLease = { - [LEASE_STATE]: { key, generation: this.currentGeneration, owner: this.owner } + const state: RequestLeaseState = { key, generation: this.currentGeneration, owner: this.owner } + const lease: RequestLease = { [LEASE_STATE]: state } + // One probe per physical request. A joiner never sees it: its `fn` is never invoked, it awaits + // this promise, and `retire()` clears `inFlight`, so no joiner can join across a generation bump. + const currency: RequestCurrency = { + isCurrent: () => state.generation === this.currentGeneration } let loaded: Promise try { // Called here rather than off a microtask so the request reaches the wire in the turn the // caller asked for it, which is what orders it against its siblings. - loaded = fn() + loaded = fn(currency) } catch (error) { loaded = Promise.reject(error instanceof Error ? error : new Error(String(error))) } diff --git a/mobile/src/transport/host-status-capability-ignorability.test.ts b/mobile/src/transport/host-status-capability-ignorability.test.ts new file mode 100644 index 00000000000..4d4e9101d3c --- /dev/null +++ b/mobile/src/transport/host-status-capability-ignorability.test.ts @@ -0,0 +1,165 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { describe, expect, it, vi } from 'vitest' +import { MOBILE_WEB_BUNDLE_CAPABILITY } from '../../../src/shared/mobile-web-bundle/mobile-web-bundle-capability' +import { MOBILE_AI_VAULT_CAPABILITY } from '../agent-history/agent-history-capability' +import { + readNewWorktreeRuntimeCapabilities, + type NewWorktreeRuntimeCapabilities +} from '../tasks/worktree-create-capability' +import { supportsMobileQuickCommands } from '../terminal/quick-commands' +import { useHostStatusGates, type HostStatusGates } from './host-status-gates' +import { + hostAnsweredStatusProbe, + readHostStatusGates, + readProbedHostCapabilities +} from './host-status-probe-operations' +import type { HostStatusReply } from './host-status-reply-schema' +import type { RpcClient } from './rpc-client' +import type { RpcResponse } from './types' + +const recordHostAppVersionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)) + +vi.mock('./host-app-version-store', () => ({ + normalizeHostAppVersion: (value: unknown) => (typeof value === 'string' ? value : null), + recordHostAppVersion: (...args: unknown[]) => recordHostAppVersionMock(...args) +})) + +/** + * What a desktop that ships a mobile web bundle now answers. The new name sits among real ones + * rather than alone, so a reader that keeps only the head or the tail of the list cannot look + * unchanged by accident, and the rest are there to make every derived state below non-trivial. + */ +const ADVERTISED_CAPABILITIES = [ + 'files.pathsExist', + MOBILE_AI_VAULT_CAPABILITY, + 'mobile.tasks.v1', + MOBILE_WEB_BUNDLE_CAPABILITY, + 'worktree.create-idempotency.v1', + 'terminal.quick-commands.v1' +] as const + +/** The old desktop is DERIVED, never written down: one string removed from what the new one sends. */ +const OLD_DESKTOP_CAPABILITIES = ADVERTISED_CAPABILITIES.filter( + (capability) => capability !== MOBILE_WEB_BUNDLE_CAPABILITY +) + +function statusReply(capabilities: readonly string[]): RpcResponse { + return { + id: 'status-1', + ok: true, + result: { + appVersion: '1.4.200', + protocolVersion: 3, + minCompatibleMobileVersion: 2, + floatingWorkspaceEnabled: true, + capabilities: [...capabilities] + }, + _meta: { runtimeId: 'runtime-1' } + } +} + +/** Answers every method with the one status reply, which is all any reader under test asks for. */ +function clientAnswering(reply: RpcResponse): RpcClient { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every reader below reaches this client only through an rpc operation's `request`, which uses sendRequest alone; the rest of RpcClient is streaming and lifecycle none of them touch. + return { sendRequest: vi.fn().mockResolvedValue(reply) } as unknown as RpcClient +} + +async function renderGates(client: RpcClient): Promise { + // Held on an object rather than in `let`s: both are written from inside a callback, where + // narrowing would read them back as their initializer. + const mount: { renderer?: ReactTestRenderer; gates?: HostStatusGates } = {} + function Probe(): null { + mount.gates = useHostStatusGates({ hostId: 'host-1', client, connState: 'connected' }) + return null + } + try { + await act(async () => { + mount.renderer = create(createElement(Probe)) + await Promise.resolve() + }) + } finally { + mount.renderer?.unmount() + } + if (!mount.gates) { + throw new Error('the gate hook never rendered') + } + return mount.gates +} + +type ClientVisibleOutcome = { + gates: HostStatusGates + gateStatus: HostStatusReply | null + probedCapabilities: readonly string[] | null + answeredProbe: boolean + quickCommandsSupported: boolean + worktreeCreateSupport: NewWorktreeRuntimeCapabilities +} + +/** Every released path that reads status.get, plus the states derived from what it published. */ +async function readEverything(capabilities: readonly string[]): Promise { + const reply = statusReply(capabilities) + const client = clientAnswering(reply) + const gates = await renderGates(client) + return { + gates, + gateStatus: readHostStatusGates(reply), + probedCapabilities: readProbedHostCapabilities(reply), + answeredProbe: hostAnsweredStatusProbe(reply), + quickCommandsSupported: supportsMobileQuickCommands(gates.hostCapabilities), + worktreeCreateSupport: await readNewWorktreeRuntimeCapabilities(client) + } +} + +function withoutBundleCapability(values: readonly string[]): string[] { + return values.filter((capability) => capability !== MOBILE_WEB_BUNDLE_CAPABILITY) +} + +/** The outcome with the one new string removed wherever it surfaced, and nothing else touched. */ +function asIfNeverAdvertised(outcome: ClientVisibleOutcome): ClientVisibleOutcome { + return { + ...outcome, + gates: { + ...outcome.gates, + hostCapabilities: withoutBundleCapability(outcome.gates.hostCapabilities) + }, + gateStatus: outcome.gateStatus + ? { + ...outcome.gateStatus, + ...(outcome.gateStatus.capabilities + ? { capabilities: withoutBundleCapability(outcome.gateStatus.capabilities) } + : {}) + } + : outcome.gateStatus, + probedCapabilities: outcome.probedCapabilities + ? withoutBundleCapability(outcome.probedCapabilities) + : outcome.probedCapabilities + } +} + +describe('mobileWeb.bundle.v1 on a released client', () => { + /** + * The Phase A promise: a desktop that starts advertising the bundle changes nothing a shipped + * phone can observe. Both sides of the comparison come from the same list, so nothing here + * records "what the old client had" and can rot out of step with it. + * + * A closed enum or an exhaustive switch over capabilities would land on the presence assertions + * first: the strict schema drops a whole salvaged field rather than one entry, so the advertised + * read would publish nothing and the equality below would fail with it. + */ + it('changes nothing a released client reads apart from the capability string itself', async () => { + const advertised = await readEverything(ADVERTISED_CAPABILITIES) + const oldDesktop = await readEverything(OLD_DESKTOP_CAPABILITIES) + + // Preconditions: without these the comparison could hold because nothing was read at all. + expect(advertised.gates.hostCapabilities).toContain(MOBILE_WEB_BUNDLE_CAPABILITY) + expect(advertised.gateStatus).not.toBeNull() + expect(advertised.probedCapabilities).toContain(MOBILE_WEB_BUNDLE_CAPABILITY) + expect(advertised.quickCommandsSupported).toBe(true) + expect(advertised.worktreeCreateSupport.tasksSupported).toBe(true) + expect(advertised.worktreeCreateSupport.worktreeCreateIdempotency).not.toBe(false) + expect(oldDesktop.gates.hostCapabilities).not.toContain(MOBILE_WEB_BUNDLE_CAPABILITY) + + expect(asIfNeverAdvertised(advertised)).toEqual(oldDesktop) + }) +}) diff --git a/mobile/src/transport/lifecycle-owner.test.ts b/mobile/src/transport/lifecycle-owner.test.ts index b8350a6b714..3a5d8aca82f 100644 --- a/mobile/src/transport/lifecycle-owner.test.ts +++ b/mobile/src/transport/lifecycle-owner.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest' import { GenerationScopedRequestOwner, type LoadedRequest, + type RequestCurrency, type RequestScope } from './generation-scoped-request-owner' @@ -277,6 +278,81 @@ describe('owner boundaries', () => { }) }) +describe('currency probe', () => { + it('flips under a request still in flight when a reset retires it', async () => { + const owner: Owner = new GenerationScopedRequestOwner() + const scope = scopeAt('w1', 1) + const request = pending() + const probes: RequestCurrency[] = [] + const loaded = owner.load(scope, QUERY, (currency) => { + probes.push(currency) + return request.start() + }) + expect(probes[0]?.isCurrent()).toBe(true) + + owner.reset() + expect(probes[0]?.isCurrent()).toBe(false) + + // The probe answers the question `commit` asks, so a loader that ignored it lands here instead. + request.resolve(['stale.ts']) + const stale = await settled(loaded) + expect(owner.commit(stale.lease, stale.value)).toBe('retired-generation') + }) + + it('flips when the next load enters on a scope the owner has not seen', async () => { + const owner: Owner = new GenerationScopedRequestOwner() + const request = pending() + const probes: RequestCurrency[] = [] + const loaded = owner.load(scopeAt('A', 1), QUERY, (currency) => { + probes.push(currency) + return request.start() + }) + expect(probes[0]?.isCurrent()).toBe(true) + + void owner.load(scopeAt('B', 1), QUERY, () => pending().start()) + expect(probes[0]?.isCurrent()).toBe(false) + + request.resolve(['a.ts']) + const stale = await settled(loaded) + expect(owner.commit(stale.lease, stale.value)).toBe('retired-generation') + }) + + it('publishes nothing and sends nothing further for a loader that stops on it', async () => { + const owner: Owner = new GenerationScopedRequestOwner() + const scope = scopeAt('w1', 1) + let sent = 0 + const firstLeg = pending() + const secondLeg = pending() + // Two legs, as the compare site has: the probe sits between them, so a superseded attempt never + // reaches the second one. + const attempt = + (leg: { start: () => Promise }) => + async (currency: RequestCurrency): Promise => { + await leg.start() + if (!currency.isCurrent()) { + return null + } + sent++ + return ['sent.ts'] + } + + const superseded = owner.load(scope, QUERY, attempt(firstLeg)) + owner.reset() + const live = owner.load(scope, QUERY, attempt(secondLeg)) + + firstLeg.resolve([]) + expect(await superseded).toBeNull() + expect(sent).toBe(0) + expect(owner.read(scope, QUERY)).toBeUndefined() + + secondLeg.resolve([]) + const lease = await settled(live) + expect(sent).toBe(1) + expect(owner.commit(lease.lease, lease.value)).toBe('committed') + expect(owner.read(scope, QUERY)).toEqual(['sent.ts']) + }) +}) + const mobileRoot = fileURLToPath(new URL('../..', import.meta.url)) const ownerModule = join(mobileRoot, 'src', 'transport', 'generation-scoped-request-owner') diff --git a/mobile/src/transport/mobile-endpoint-supervisor-test-fakes.ts b/mobile/src/transport/mobile-endpoint-supervisor-test-fakes.ts index 80f4438c160..e026ea26889 100644 --- a/mobile/src/transport/mobile-endpoint-supervisor-test-fakes.ts +++ b/mobile/src/transport/mobile-endpoint-supervisor-test-fakes.ts @@ -26,7 +26,8 @@ export class FakeSession implements RpcClient { getState = () => this.state getReconnectAttempt = () => 0 - getLastConnectedAt = () => null + // Nullable: the escalation suites replace this with a real timestamp. + getLastConnectedAt: () => number | null = () => null onStateChange = (listener: (state: ConnectionState) => void) => { this.listeners.add(listener) return () => this.listeners.delete(listener) diff --git a/mobile/src/transport/mobile-relay-rpc-streams.test.ts b/mobile/src/transport/mobile-relay-rpc-streams.test.ts index b0e59c6bf5b..78cde925330 100644 --- a/mobile/src/transport/mobile-relay-rpc-streams.test.ts +++ b/mobile/src/transport/mobile-relay-rpc-streams.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { RpcFailure } from './types' +import type { RpcFailure, RpcSuccess } from './types' import { MobileRelayRpcStreams } from './mobile-relay-rpc-streams' function rpcFailure(id: string): RpcFailure { @@ -150,3 +150,48 @@ describe('MobileRelayRpcStreams failure parity', () => { expect(listener).toHaveBeenCalledTimes(1) }) }) + +function readyReply(id: string): RpcSuccess { + return { + id, + ok: true, + streaming: true, + result: { type: 'ready', subscriptionId: 'sub-1' }, + _meta: { runtimeId: 'runtime-1' } + } +} + +describe('MobileRelayRpcStreams cancel fencing', () => { + function subscribed() { + const listener = vi.fn() + const streams = new MobileRelayRpcStreams({ + nextId: () => 'stream-1', + sendFrame: vi.fn(() => true), + waitForConnected: async () => {} + }) + const cancel = streams.subscribe( + 'notifications.subscribe', + { includeDesktopSuppressed: true }, + listener + ) + return { listener, streams, cancel } + } + + it('delivers a ready reply to a live subscription', async () => { + const { listener, streams } = subscribed() + await Promise.resolve() + + expect(streams.handleResponse(readyReply('stream-1'))).toBe(true) + expect(listener).toHaveBeenCalledExactlyOnceWith({ type: 'ready', subscriptionId: 'sub-1' }) + }) + + it('drops a ready reply that lands after the caller cancelled', async () => { + const { listener, streams, cancel } = subscribed() + await Promise.resolve() + + cancel() + + expect(streams.handleResponse(readyReply('stream-1'))).toBe(false) + expect(listener).not.toHaveBeenCalled() + }) +}) diff --git a/mobile/src/transport/mobile-runtime-host-platform.ts b/mobile/src/transport/mobile-runtime-host-platform.ts index cf7b13aa283..03cbcd390a1 100644 --- a/mobile/src/transport/mobile-runtime-host-platform.ts +++ b/mobile/src/transport/mobile-runtime-host-platform.ts @@ -1,16 +1,21 @@ -const NODE_PLATFORMS = new Set([ - 'aix', - 'android', - 'darwin', - 'freebsd', - 'haiku', - 'linux', - 'openbsd', - 'sunos', - 'win32', - 'cygwin', - 'netbsd' -]) +import { hostUnionArms } from '../../../src/shared/zod-salvage' + +/** Node's own platform domain, pinned to @types/node's union: an arm added or dropped there fails tsc here. */ +export const NODE_PLATFORM_NAMES = hostUnionArms({ + aix: true, + android: true, + darwin: true, + freebsd: true, + haiku: true, + linux: true, + openbsd: true, + sunos: true, + win32: true, + cygwin: true, + netbsd: true +}) + +const NODE_PLATFORMS = new Set(NODE_PLATFORM_NAMES) export function readMobileRuntimeHostPlatform(statusResult: unknown): NodeJS.Platform | null { const hostPlatform = (statusResult as { hostPlatform?: unknown } | null)?.hostPlatform diff --git a/mobile/src/transport/mobile-web-bundle-fetch.test.ts b/mobile/src/transport/mobile-web-bundle-fetch.test.ts new file mode 100644 index 00000000000..70856553407 --- /dev/null +++ b/mobile/src/transport/mobile-web-bundle-fetch.test.ts @@ -0,0 +1,530 @@ +import { sha256 } from '@noble/hashes/sha256' +import { describe, expect, it, vi } from 'vitest' +import { fetchMobileWebBundle } from './mobile-web-bundle-fetch' +import { readMobileWebBundleErrorCode } from './mobile-web-bundle-operations' +import type { RpcClient } from './rpc-client' +import type { RpcResponse } from './types' + +const BUILD_ID = 'a'.repeat(64) + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('') +} + +function encodeBase64(bytes: Uint8Array): string { + return btoa(String.fromCharCode(...bytes)) +} + +function bytesOf(text: string): Uint8Array { + return new TextEncoder().encode(text) +} + +type HostCall = { method: string; params: unknown } + +type HostOptions = { + chunkBytes?: number + buildId?: string + /** Replaces the reply the host would have sent for this request. */ + intercept?: (call: HostCall) => unknown + onInFlight?: (inFlight: number) => void +} + +/** Box first, so params that are not an object read as absent instead of throwing. */ +function paramField(params: unknown, key: string): unknown { + const boxed: Record = Object(params) + return boxed[key] +} + +/** A host that serves a fixed asset table by the same rules the real one does. */ +function bundleHost(files: Record, options: HostOptions = {}) { + const chunkBytes = options.chunkBytes ?? 4 + const buildId = options.buildId ?? BUILD_ID + const bytes = new Map(Object.entries(files).map(([path, text]) => [path, bytesOf(text)])) + const assets = [...bytes.entries()] + .map(([path, content]) => ({ + path, + sha256: toHex(sha256(content)), + byteLength: content.byteLength, + contentType: 'text/plain' + })) + .sort((left, right) => (left.path < right.path ? -1 : 1)) + const manifest = { + schemaVersion: 1, + buildId, + desktopVersion: '1.4.200', + minCompatibleRuntimeProtocolVersion: 2, + runtimeProtocolVersion: 2, + entrypoint: assets[0]!.path, + totalBytes: assets.reduce((total, entry) => total + entry.byteLength, 0), + assets + } + const calls: HostCall[] = [] + let inFlight = 0 + + const answer = (method: string, params: unknown): unknown => { + if (method === 'mobileWeb.bundle.manifest') { + return { manifest, chunkBytes } + } + const path = String(paramField(params, 'path')) + const offset = Number(paramField(params, 'offset')) + const content = bytes.get(path)! + const slice = content.subarray(offset, offset + chunkBytes) + return { + buildId, + path, + offset, + assetByteLength: content.byteLength, + sha256: toHex(sha256(content)), + dataBase64: encodeBase64(slice), + eof: offset + slice.byteLength >= content.byteLength + } + } + + const client: RpcClient = { + sendRequest: vi.fn(async (method: string, params?: unknown): Promise => { + const call: HostCall = { method, params: params ?? {} } + calls.push(call) + inFlight += 1 + options.onInFlight?.(inFlight) + try { + await new Promise((resolve) => setTimeout(resolve, 0)) + const replaced = options.intercept?.(call) + const result = replaced === undefined ? answer(method, call.params) : replaced + if (result instanceof Error) { + return { + id: 'rpc-1', + ok: false, + error: { code: 'invalid_argument', message: result.message }, + _meta: { runtimeId: 'runtime-1' } + } + } + return { id: 'rpc-1', ok: true, result, _meta: { runtimeId: 'runtime-1' } } + } finally { + inFlight -= 1 + } + }), + subscribe: vi.fn(() => () => {}), + updateTerminalSubscriptionViewport: vi.fn(), + getState: () => 'connected', + getReconnectAttempt: () => 0, + getLastConnectedAt: () => 1, + onStateChange: () => () => {}, + notifyForeground: vi.fn(), + close: vi.fn() + } + return { client, calls, manifest } +} + +function chunkCallCount(calls: readonly HostCall[]): number { + return calls.filter((call) => call.method === 'mobileWeb.bundle.chunk').length +} + +/** Long enough for an unstopped worker pool to page three 40-byte assets one byte at a time. */ +async function drainPendingHostWork(): Promise { + for (let tick = 0; tick < 300; tick += 1) { + await new Promise((resolve) => setTimeout(resolve, 0)) + } +} + +describe('fetchMobileWebBundle', () => { + it('pages every asset to eof and returns the verified bytes', async () => { + const host = bundleHost({ 'index.html': '

orca

', 'assets/app.js': 'x=1' }) + const progress: number[] = [] + + const fetched = await fetchMobileWebBundle({ + client: host.client, + onProgress: (update) => progress.push(update.receivedBytes) + }) + + expect([...fetched.assets.keys()].sort()).toEqual(['assets/app.js', 'index.html']) + expect(new TextDecoder().decode(fetched.assets.get('index.html'))).toBe('

orca

') + expect(new TextDecoder().decode(fetched.assets.get('assets/app.js'))).toBe('x=1') + expect(fetched.totalBytes).toBe(16) + expect(fetched.manifest.buildId).toBe(BUILD_ID) + expect(fetched.elapsedMs).toBeGreaterThanOrEqual(0) + expect(progress).toHaveLength(2) + expect(progress.at(-1)).toBe(16) + // 13 bytes at 4 per chunk is four requests, 3 bytes is one, plus the manifest. + expect(host.calls.filter((call) => call.method === 'mobileWeb.bundle.chunk')).toHaveLength(5) + expect(host.calls[0]!.method).toBe('mobileWeb.bundle.manifest') + expect(host.calls[0]!.params).toEqual({}) + }) + + it('asks for each chunk at the offset the previous reply ended on', async () => { + const host = bundleHost({ 'index.html': 'abcdefghij' }, { chunkBytes: 3 }) + + await fetchMobileWebBundle({ client: host.client }) + + expect( + host.calls + .filter((call) => call.method === 'mobileWeb.bundle.chunk') + .map((call) => call.params) + ).toEqual([ + { buildId: BUILD_ID, path: 'index.html', offset: 0 }, + { buildId: BUILD_ID, path: 'index.html', offset: 3 }, + { buildId: BUILD_ID, path: 'index.html', offset: 6 }, + { buildId: BUILD_ID, path: 'index.html', offset: 9 } + ]) + }) + + it('fails when a reassembled asset does not hash to the manifest entry', async () => { + const host = bundleHost( + { 'index.html': 'abcdef' }, + { + chunkBytes: 3, + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' && paramField(call.params, 'offset') === 3 + ? { + buildId: BUILD_ID, + path: 'index.html', + offset: 3, + assetByteLength: 6, + sha256: toHex(sha256(bytesOf('abcdef'))), + dataBase64: encodeBase64(bytesOf('XYZ')), + eof: true + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + /index\.html hashed [0-9a-f]{64}, not/ + ) + }) + + it('fails when the host serves a later chunk from a different build', async () => { + const host = bundleHost( + { 'index.html': 'abcdef' }, + { + chunkBytes: 3, + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' && paramField(call.params, 'offset') === 3 + ? { + buildId: 'c'.repeat(64), + path: 'index.html', + offset: 3, + assetByteLength: 6, + sha256: toHex(sha256(bytesOf('abcdef'))), + dataBase64: encodeBase64(bytesOf('def')), + eof: true + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + 'bundle build changed mid-fetch' + ) + }) + + it('refuses a chunk that answers a different path or offset', async () => { + const host = bundleHost( + { 'index.html': 'abc' }, + { + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' + ? { + buildId: BUILD_ID, + path: 'other.html', + offset: 0, + assetByteLength: 3, + sha256: toHex(sha256(bytesOf('abc'))), + dataBase64: encodeBase64(bytesOf('abc')), + eof: true + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + 'bundle chunk answered other.html at 0, not index.html at 0' + ) + }) + + it('refuses a chunk that answers the right path at the wrong offset', async () => { + // The path half of the echo check is already covered; this is the offset half on its own, so + // a host that re-serves chunk zero cannot have its bytes written at the offset we asked for. + const host = bundleHost( + { 'index.html': 'abcdef' }, + { + chunkBytes: 3, + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' && paramField(call.params, 'offset') === 3 + ? { + buildId: BUILD_ID, + path: 'index.html', + offset: 0, + assetByteLength: 6, + sha256: toHex(sha256(bytesOf('abcdef'))), + dataBase64: encodeBase64(bytesOf('abc')), + eof: false + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + 'bundle chunk answered index.html at 0, not index.html at 3' + ) + }) + + it('reads a zero-byte asset in one chunk and returns it empty', async () => { + // A real bundle carries these. The asset is whole the moment the host says eof, and nothing + // else in the loop can end it: a zero-length reply is otherwise how a host makes no progress. + const host = bundleHost({ 'assets/empty.css': '', 'index.html': 'abc' }) + + const fetched = await fetchMobileWebBundle({ client: host.client }) + + expect(fetched.assets.get('assets/empty.css')).toEqual(new Uint8Array(0)) + expect(fetched.totalBytes).toBe(3) + expect( + host.calls.filter( + (call) => + call.method === 'mobileWeb.bundle.chunk' && + paramField(call.params, 'path') === 'assets/empty.css' + ) + ).toHaveLength(1) + }) + + it('never puts a fifth chunk request on one connection', async () => { + const peaks: number[] = [] + const host = bundleHost( + Object.fromEntries( + Array.from({ length: 9 }, (_, index) => [`assets/${index}.js`, `body-${index}`]) + ), + { chunkBytes: 2, onInFlight: (inFlight) => peaks.push(inFlight) } + ) + + const fetched = await fetchMobileWebBundle({ client: host.client }) + + expect(fetched.assets.size).toBe(9) + expect(Math.max(...peaks)).toBe(4) + }) + + it('stops as soon as the caller aborts', async () => { + const controller = new AbortController() + const host = bundleHost({ 'index.html': 'abcdefgh' }, { chunkBytes: 2 }) + + const started = fetchMobileWebBundle({ client: host.client, signal: controller.signal }) + controller.abort() + + await expect(started).rejects.toThrow('mobile web bundle fetch aborted') + expect(host.calls.filter((call) => call.method === 'mobileWeb.bundle.chunk')).toHaveLength(0) + }) + + it('refuses a chunk larger than the size the host advertised', async () => { + const host = bundleHost( + { 'index.html': 'abcdef' }, + { + chunkBytes: 3, + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' + ? { + buildId: BUILD_ID, + path: 'index.html', + offset: 0, + assetByteLength: 6, + sha256: toHex(sha256(bytesOf('abcdef'))), + dataBase64: encodeBase64(bytesOf('abcdef')), + eof: true + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + "bundle chunk for index.html at 0 is 6 bytes, over the host's 3" + ) + }) + + it('refuses a chunk whose asset no longer matches the manifest entry', async () => { + const host = bundleHost( + { 'index.html': 'abc' }, + { + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' + ? { + buildId: BUILD_ID, + path: 'index.html', + offset: 0, + assetByteLength: 4, + sha256: toHex(sha256(bytesOf('abc'))), + dataBase64: encodeBase64(bytesOf('abc')), + eof: true + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + 'bundle asset index.html no longer matches the manifest entry' + ) + }) + + it('refuses an asset that ends short of the length the manifest declares', async () => { + const host = bundleHost( + { 'index.html': 'abcdef' }, + { + chunkBytes: 3, + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' + ? { + buildId: BUILD_ID, + path: 'index.html', + offset: 0, + assetByteLength: 6, + sha256: toHex(sha256(bytesOf('abcdef'))), + dataBase64: encodeBase64(bytesOf('abc')), + eof: true + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + 'bundle asset index.html ended at 3 of 6 declared bytes' + ) + }) + + it('stops a host that pages forever without sending a byte', async () => { + const host = bundleHost( + { 'index.html': 'abcdef' }, + { + chunkBytes: 3, + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' + ? { + buildId: BUILD_ID, + path: 'index.html', + offset: 0, + assetByteLength: 6, + sha256: toHex(sha256(bytesOf('abcdef'))), + dataBase64: '', + eof: false + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + 'bundle asset index.html made no progress at 0' + ) + }) + + it('stops the other workers mid-asset once one asset is refused', async () => { + const host = bundleHost( + { + 'a.js': 'x', + 'b.js': 'b'.repeat(40), + 'c.js': 'c'.repeat(40), + 'd.js': 'd'.repeat(40) + }, + { + chunkBytes: 1, + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' && paramField(call.params, 'path') === 'a.js' + ? new Error('mobile_web_bundle_asset_unknown') + : undefined + } + ) + + const error = await fetchMobileWebBundle({ client: host.client }).catch( + (thrown: unknown) => thrown + ) + const atRejection = chunkCallCount(host.calls) + await drainPendingHostWork() + + // The refusal is what the caller sees; the internal stop never surfaces. + expect(readMobileWebBundleErrorCode(error)).toBe('mobile_web_bundle_asset_unknown') + // 120 chunks would page the other three assets to the end. One more round of four is the most + // the abandoned workers can add, because each checks the stop before it asks for a chunk. + expect(chunkCallCount(host.calls)).toBeLessThanOrEqual(atRejection + 4) + expect(chunkCallCount(host.calls)).toBeLessThan(10) + }) + + it('asks for nothing at all when the caller arrives already aborted', async () => { + const host = bundleHost({ 'index.html': 'abc' }) + + await expect( + fetchMobileWebBundle({ client: host.client, signal: AbortSignal.abort() }) + ).rejects.toThrow('mobile web bundle fetch aborted') + expect(host.calls).toHaveLength(0) + }) + + it('refuses an asset the host over-pages with real bytes', async () => { + const host = bundleHost( + { 'index.html': 'abcdef' }, + { + chunkBytes: 3, + // Never says eof, so the third reply writes past the six bytes the manifest declares. + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' + ? { + buildId: BUILD_ID, + path: 'index.html', + offset: paramField(call.params, 'offset'), + assetByteLength: 6, + sha256: toHex(sha256(bytesOf('abcdef'))), + dataBase64: encodeBase64(bytesOf('abc')), + eof: false + } + : undefined + } + ) + + await expect(fetchMobileWebBundle({ client: host.client })).rejects.toThrow( + 'bundle asset index.html is longer than the manifest declares' + ) + }) + + it('counts the bytes it received rather than the total the manifest claims', async () => { + const host = bundleHost({ 'index.html': 'abcdef' }, { chunkBytes: 3 }) + // The mobile reader is loose, so it does not carry the host schema's sum refinement: a manifest + // whose total disagrees with its assets must not decide what the fetch reports. + host.manifest.totalBytes = 999 + + const fetched = await fetchMobileWebBundle({ client: host.client }) + + expect(fetched.totalBytes).toBe(6) + }) + + it('reads a schema refusal whose message is prose as a generic failure', async () => { + // The dispatcher refuses params that fail the host schema before the bundle handler runs, so the + // message is zod prose rather than one of the six codes. + const host = bundleHost( + { 'index.html': 'abc' }, + { + intercept: (call) => + call.method === 'mobileWeb.bundle.chunk' + ? new Error('Invalid input: expected string, received number') + : undefined + } + ) + + const error = await fetchMobileWebBundle({ client: host.client }).catch( + (thrown: unknown) => thrown + ) + + expect(readMobileWebBundleErrorCode(error)).toBeNull() + expect(error).toBeInstanceOf(Error) + expect(String(error)).toContain('Invalid input: expected string, received number') + }) + + it('surfaces the host code when the bundle is not there to serve', async () => { + const host = bundleHost( + { 'index.html': 'abc' }, + { + intercept: (call) => + call.method === 'mobileWeb.bundle.manifest' + ? new Error('mobile_web_bundle_unavailable') + : undefined + } + ) + + const error = await fetchMobileWebBundle({ client: host.client }).catch( + (thrown: unknown) => thrown + ) + + expect(readMobileWebBundleErrorCode(error)).toBe('mobile_web_bundle_unavailable') + }) +}) diff --git a/mobile/src/transport/mobile-web-bundle-fetch.ts b/mobile/src/transport/mobile-web-bundle-fetch.ts new file mode 100644 index 00000000000..2823085437a --- /dev/null +++ b/mobile/src/transport/mobile-web-bundle-fetch.ts @@ -0,0 +1,192 @@ +import { sha256 } from '@noble/hashes/sha256' +import { + mobileWebBundleChunkRead, + mobileWebBundleManifestRead +} from './mobile-web-bundle-operations' +import type { + MobileWebBundleAssetRead, + MobileWebBundleManifestRead +} from './mobile-web-bundle-reply-schemas' +import type { RpcClient } from './rpc-client' +import { runRpcOperation } from './rpc-operation' + +/** The host refuses the fifth concurrent read on one connection with `mobile_web_bundle_read_limited`, + * so the client never offers a fifth. Paging inside one asset stays sequential: the next offset is + * only known to be wanted once the previous reply says it is not the last. */ +const MAX_CONCURRENT_ASSET_READS = 4 + +export type MobileWebBundleFetchProgress = { + readonly completedAssets: number + readonly totalAssets: number + readonly receivedBytes: number + readonly totalBytes: number +} + +export type MobileWebBundleFetchResult = { + readonly manifest: MobileWebBundleManifestRead + readonly assets: ReadonlyMap + readonly totalBytes: number + readonly elapsedMs: number +} + +/** + * Reads the manifest, pages every asset, and returns the verified bytes. + * + * Nothing is cached and nothing is rendered: this is the Phase A proof that the pipe carries a whole + * bundle intact. Every asset is checked against the manifest's own sha256 before it is returned, so + * a truncated or reordered reassembly fails here rather than in a webview much later. + */ +export async function fetchMobileWebBundle(args: { + client: RpcClient + signal?: AbortSignal + onProgress?: (progress: MobileWebBundleFetchProgress) => void +}): Promise { + const startedAt = Date.now() + const stopped = new AbortController() + throwIfStopped(args.signal, stopped.signal) + const opened = await runRpcOperation(args.client, mobileWebBundleManifestRead, null) + const manifest = opened.manifest + const pending = [...manifest.assets] + const assets = new Map() + let receivedBytes = 0 + + const worker = async (): Promise => { + try { + for (let asset = pending.shift(); asset !== undefined; asset = pending.shift()) { + const bytes = await readBundleAsset({ + client: args.client, + asset, + buildId: manifest.buildId, + chunkBytes: opened.chunkBytes, + signal: args.signal, + stopped: stopped.signal + }) + assets.set(asset.path, bytes) + receivedBytes += bytes.byteLength + args.onProgress?.({ + completedAssets: assets.size, + totalAssets: manifest.assets.length, + receivedBytes, + totalBytes: manifest.totalBytes + }) + } + } catch (error) { + // One failed asset stops the other three mid-asset, not just between assets: every chunk they + // would still ask for holds one of the host's four read slots against the caller's retry. + stopped.abort() + throw error + } + } + + const workers = Math.min(MAX_CONCURRENT_ASSET_READS, pending.length) + await Promise.all(Array.from({ length: workers }, () => worker())) + return { manifest, assets, totalBytes: receivedBytes, elapsedMs: Date.now() - startedAt } +} + +async function readBundleAsset(args: { + client: RpcClient + asset: MobileWebBundleAssetRead + buildId: string + chunkBytes: number + signal?: AbortSignal + stopped: AbortSignal +}): Promise { + // Before the buffer, not after: an asset can be a tenth of the total ceiling, and a worker that + // picked one up after a sibling failed would otherwise allocate it only to drop it. + throwIfStopped(args.signal, args.stopped) + const whole = new Uint8Array(args.asset.byteLength) + let offset = 0 + for (;;) { + throwIfStopped(args.signal, args.stopped) + const chunk = await runRpcOperation(args.client, mobileWebBundleChunkRead, { + buildId: args.buildId, + path: args.asset.path, + offset + }) + assertChunkDescribesAsset(chunk, args.asset, args.buildId, offset) + const bytes = decodeBase64(chunk.dataBase64) + if (bytes.byteLength > args.chunkBytes) { + throw new Error( + `bundle chunk for ${args.asset.path} at ${offset} is ${bytes.byteLength} bytes, over the host's ${args.chunkBytes}` + ) + } + if (offset + bytes.byteLength > whole.byteLength) { + throw new Error(`bundle asset ${args.asset.path} is longer than the manifest declares`) + } + whole.set(bytes, offset) + offset += bytes.byteLength + if (chunk.eof) { + break + } + // Without this a host that keeps answering an unchanged offset with no bytes pages forever. + if (bytes.byteLength === 0) { + throw new Error(`bundle asset ${args.asset.path} made no progress at ${offset}`) + } + } + if (offset !== whole.byteLength) { + throw new Error( + `bundle asset ${args.asset.path} ended at ${offset} of ${whole.byteLength} declared bytes` + ) + } + const digest = toHex(sha256(whole)) + if (digest !== args.asset.sha256) { + throw new Error(`bundle asset ${args.asset.path} hashed ${digest}, not ${args.asset.sha256}`) + } + return whole +} + +/** + * Every chunk reply restates the build, path and offset it answers, and the whole asset's length and + * hash. Checking all five is what makes a misrouted or stale reply a failure here instead of a + * corrupt reassembly: a desktop that auto-updates mid-download answers a later chunk from a + * different build, and nothing else in the reply would say so. + */ +function assertChunkDescribesAsset( + chunk: { + buildId: string + path: string + offset: number + assetByteLength: number + sha256: string + }, + asset: MobileWebBundleAssetRead, + buildId: string, + offset: number +): void { + if (chunk.buildId !== buildId) { + throw new Error(`bundle build changed mid-fetch: asked ${buildId}, served ${chunk.buildId}`) + } + if (chunk.path !== asset.path || chunk.offset !== offset) { + throw new Error( + `bundle chunk answered ${chunk.path} at ${chunk.offset}, not ${asset.path} at ${offset}` + ) + } + if (chunk.sha256 !== asset.sha256 || chunk.assetByteLength !== asset.byteLength) { + throw new Error(`bundle asset ${asset.path} no longer matches the manifest entry`) + } +} + +/** The caller's abort is what it asked for; the internal one never leaves this module, because the + * asset that failed rejects first and is what `Promise.all` reports. */ +function throwIfStopped(caller: AbortSignal | undefined, stopped: AbortSignal): void { + if (caller?.aborted === true) { + throw new Error('mobile web bundle fetch aborted') + } + if (stopped.aborted) { + throw new Error('mobile web bundle fetch stopped after an earlier asset failed') + } +} + +/** Metro ships no Buffer; `atob` is the decoder the pairing and E2EE paths already run on Hermes. */ +function decodeBase64(value: string): Uint8Array { + const binary = atob(value) + const bytes = new Uint8Array(binary.length) + for (let index = 0; index < binary.length; index += 1) { + bytes[index] = binary.charCodeAt(index) + } + return bytes +} + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('') +} diff --git a/mobile/src/transport/mobile-web-bundle-operations.ts b/mobile/src/transport/mobile-web-bundle-operations.ts new file mode 100644 index 00000000000..accd27bdfe5 --- /dev/null +++ b/mobile/src/transport/mobile-web-bundle-operations.ts @@ -0,0 +1,77 @@ +import { + MobileWebBundleErrorCodeSchema, + MOBILE_WEB_BUNDLE_CHUNK_METHOD, + MOBILE_WEB_BUNDLE_MANIFEST_METHOD, + type MobileWebBundleErrorCode +} from '../../../src/shared/mobile-web-bundle/bundle-rpc-contract' +import { + MobileWebBundleChunkReplySchema, + MobileWebBundleManifestReplySchema +} from './mobile-web-bundle-reply-schemas' +import { defineRpcOperation } from './rpc-operation' +import { rpcResultVariant } from './rpc-operation-result-reader' + +// The two reads that hand a paired phone the desktop's mobile web bundle. Both are +// `require-result-or-throw`: there is no partial success here, and a salvage policy would produce a +// half-bundle that fails a hash check much later, far from the cause. Both settle at `on-settle`, +// because each reply is acted on before the next request is built — the manifest decides which +// assets to page, and a chunk decides the next offset. + +/** The whole manifest plus the chunk size the host will serve it at. */ +export const mobileWebBundleManifestRead = defineRpcOperation({ + name: 'mobileWeb.bundle-manifest', + method: MOBILE_WEB_BUNDLE_MANIFEST_METHOD, + acceptance: 'require-result-or-throw', + barrier: 'on-settle', + read: rpcResultVariant('mobile-web-bundle-manifest', MobileWebBundleManifestReplySchema) +}) + +/** One page of one asset, self-describing so a misplaced reply cannot corrupt a reassembly. */ +export const mobileWebBundleChunkRead = defineRpcOperation({ + name: 'mobileWeb.bundle-chunk', + method: MOBILE_WEB_BUNDLE_CHUNK_METHOD, + acceptance: 'require-result-or-throw', + barrier: 'on-settle', + read: rpcResultVariant('mobile-web-bundle-chunk', MobileWebBundleChunkReplySchema) +}) + +/** A code is a bare snake_case token, so only the two positions one can occupy are read. */ +const LEADING_CODE_TOKEN = /^[a-z][a-z0-9_]*/ + +/** + * The host's six codes, read back off a thrown refusal. + * + * The host raises these as `InvalidArgumentError`, which the dispatcher maps to envelope code + * `invalid_argument` with the machine code as the message + * (`src/main/runtime/rpc/dispatcher-error-response.ts`), and `require-result-or-throw` throws + * `` `${code}: ${message}` ``. So the token this branches on is either the whole message or what + * follows the envelope code, and nowhere else: scanning the prose for a code anywhere would let a + * host that merely quoted one back read as that failure. + * + * Returns null for every other error, including a transport rejection, which is not a verdict about + * the bundle at all. Membership is decided by the contract's own enum, so the arms cannot drift + * from `MOBILE_WEB_BUNDLE_ERROR_CODES`. + */ +export function readMobileWebBundleErrorCode(error: unknown): MobileWebBundleErrorCode | null { + if (!(error instanceof Error)) { + return null + } + const head = LEADING_CODE_TOKEN.exec(error.message)?.[0] + if (head === undefined) { + return null + } + const direct = MobileWebBundleErrorCodeSchema.safeParse(head) + if (direct.success) { + return direct.data + } + const prefix = `${head}: ` + if (!error.message.startsWith(prefix)) { + return null + } + const nested = LEADING_CODE_TOKEN.exec(error.message.slice(prefix.length))?.[0] + if (nested === undefined) { + return null + } + const parsed = MobileWebBundleErrorCodeSchema.safeParse(nested) + return parsed.success ? parsed.data : null +} diff --git a/mobile/src/transport/mobile-web-bundle-reply-schemas.test.ts b/mobile/src/transport/mobile-web-bundle-reply-schemas.test.ts new file mode 100644 index 00000000000..3b80906d836 --- /dev/null +++ b/mobile/src/transport/mobile-web-bundle-reply-schemas.test.ts @@ -0,0 +1,301 @@ +import { describe, expect, it } from 'vitest' +import { + MOBILE_WEB_BUNDLE_CHUNK_BYTES, + MOBILE_WEB_BUNDLE_ERROR_CODES +} from '../../../src/shared/mobile-web-bundle/bundle-rpc-contract' +import { + MOBILE_WEB_BUNDLE_MAX_ASSETS, + MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES, + MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES +} from '../../../src/shared/mobile-web-bundle/manifest-contract' +import { + mobileWebBundleChunkRead, + mobileWebBundleManifestRead, + readMobileWebBundleErrorCode +} from './mobile-web-bundle-operations' +import type { RpcReadResult } from './rpc-operation-contract' + +const BUILD_ID = 'a'.repeat(64) +const ASSET_SHA = 'b'.repeat(64) +const MAX_DATA_BASE64_LENGTH = Math.ceil(MOBILE_WEB_BUNDLE_CHUNK_BYTES / 3) * 4 + 8 + +function asset(overrides: Record = {}) { + return { + path: 'index.html', + sha256: ASSET_SHA, + byteLength: 12, + contentType: 'text/html; charset=utf-8', + ...overrides + } +} + +function manifestReply(overrides: Record = {}) { + return { + manifest: { + schemaVersion: 1, + buildId: BUILD_ID, + desktopVersion: '1.4.200', + minCompatibleRuntimeProtocolVersion: 2, + runtimeProtocolVersion: 2, + entrypoint: 'index.html', + totalBytes: 12, + assets: [asset()], + ...overrides + }, + chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES + } +} + +function chunkReply(overrides: Record = {}) { + return { + buildId: BUILD_ID, + path: 'index.html', + offset: 0, + assetByteLength: 12, + sha256: ASSET_SHA, + dataBase64: 'aGVsbG8=', + eof: true, + ...overrides + } +} + +function readManifest(raw: unknown): RpcReadResult { + return mobileWebBundleManifestRead.read(raw) +} + +function readChunk(raw: unknown): RpcReadResult { + return mobileWebBundleChunkRead.read(raw) +} + +describe('mobile web bundle manifest reply reader', () => { + it('reads a manifest and keeps every member an unknown key carries', () => { + const result = readManifest({ + ...manifestReply(), + manifest: { ...manifestReply().manifest, contentEncoding: 'br' }, + servedFrom: 'asar' + }) + + expect(result.compatible).toBe(true) + if (!result.compatible) { + return + } + expect(result.variant).toBe('mobile-web-bundle-manifest') + expect(result.salvage).toEqual({ droppedPaths: [], droppedCount: 0 }) + expect(result.value).toMatchObject({ + servedFrom: 'asar', + chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES, + manifest: { contentEncoding: 'br', buildId: BUILD_ID } + }) + }) + + it('accepts a host that shrank chunkBytes and refuses one that grew it', () => { + expect(readManifest({ ...manifestReply(), chunkBytes: 1 }).compatible).toBe(true) + expect( + readManifest({ ...manifestReply(), chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES }).compatible + ).toBe(true) + for (const chunkBytes of [0, -1, 1.5, MOBILE_WEB_BUNDLE_CHUNK_BYTES + 1]) { + expect(readManifest({ ...manifestReply(), chunkBytes }).compatible).toBe(false) + } + }) + + it('bounds what a manifest can make the fetch allocate, however it declares totalBytes', () => { + // The ceilings above bound each asset and the asset count, and `totalBytes` separately. None + // of them bounds the product, which is what the fetch allocates. + const oversized = Array.from({ length: MOBILE_WEB_BUNDLE_MAX_ASSETS }, (_, index) => + asset({ path: `assets/${index}.js`, byteLength: MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES }) + ) + expect(readManifest(manifestReply({ totalBytes: 0, assets: oversized })).compatible).toBe(false) + expect(readManifest(manifestReply({ totalBytes: 12, assets: oversized })).compatible).toBe( + false + ) + }) + + it('accepts a bundle that sums to the ceiling and refuses one byte more', () => { + // Four assets, because one quarter of the total ceiling is the largest share that still fits + // under the per-asset ceiling. `lastByteLength` moves only the final one. + const quarter = MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES / 4 + const spread = (lastByteLength: number) => + Array.from({ length: 4 }, (_, index) => + asset({ path: `assets/${index}.js`, byteLength: index === 3 ? lastByteLength : quarter }) + ) + expect( + readManifest( + manifestReply({ totalBytes: MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES, assets: spread(quarter) }) + ).compatible + ).toBe(true) + expect( + readManifest( + manifestReply({ + totalBytes: MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES, + assets: spread(quarter + 1) + }) + ).compatible + ).toBe(false) + }) + + it('refuses one asset over the per-asset ceiling and accepts one at it', () => { + expect( + readManifest( + manifestReply({ + totalBytes: MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES, + assets: [asset({ byteLength: MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES })] + }) + ).compatible + ).toBe(true) + expect( + readManifest( + manifestReply({ assets: [asset({ byteLength: MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES + 1 })] }) + ).compatible + ).toBe(false) + }) + + it('refuses a schemaVersion it does not know rather than guessing at the shape', () => { + expect(readManifest(manifestReply({ schemaVersion: 2 })).compatible).toBe(false) + expect(readManifest(manifestReply({ schemaVersion: undefined })).compatible).toBe(false) + }) + + it('bounds every manifest field the fetch reads', () => { + expect(readManifest(manifestReply({ buildId: 'A'.repeat(64) })).compatible).toBe(false) + expect(readManifest(manifestReply({ buildId: 'a'.repeat(63) })).compatible).toBe(false) + expect(readManifest(manifestReply({ assets: [] })).compatible).toBe(false) + expect( + readManifest( + manifestReply({ + assets: Array.from({ length: MOBILE_WEB_BUNDLE_MAX_ASSETS + 1 }, (_, index) => + asset({ path: `assets/${index}.js` }) + ) + }) + ).compatible + ).toBe(false) + expect( + readManifest(manifestReply({ totalBytes: MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES + 1 })).compatible + ).toBe(false) + expect(readManifest(manifestReply({ entrypoint: undefined })).compatible).toBe(false) + expect(readManifest(manifestReply({ entrypoint: '../escape.html' })).compatible).toBe(false) + }) + + it('bounds every asset field and rejects a path that could leave the bundle root', () => { + for (const path of ['../evil.js', '/abs.js', 'a\\b.js', 'nul.js', 'trailing.']) { + expect(readManifest(manifestReply({ assets: [asset({ path })] })).compatible).toBe(false) + } + expect(readManifest(manifestReply({ assets: [asset({ sha256: 'zz' })] })).compatible).toBe( + false + ) + expect( + readManifest( + manifestReply({ assets: [asset({ byteLength: MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES + 1 })] }) + ).compatible + ).toBe(false) + expect(readManifest(manifestReply({ assets: [asset({ byteLength: -1 })] })).compatible).toBe( + false + ) + expect(readManifest(manifestReply({ assets: [asset({ contentType: '' })] })).compatible).toBe( + false + ) + }) +}) + +describe('mobile web bundle chunk reply reader', () => { + it('reads a chunk and passes an unknown member through', () => { + const result = readChunk({ ...chunkReply(), contentEncoding: 'br' }) + + expect(result.compatible).toBe(true) + if (!result.compatible) { + return + } + expect(result.variant).toBe('mobile-web-bundle-chunk') + expect(result.value).toMatchObject({ contentEncoding: 'br', eof: true, offset: 0 }) + expect(result.salvage.droppedCount).toBe(0) + }) + + it('bounds dataBase64 at the chunk size the contract allows', () => { + expect( + readChunk(chunkReply({ dataBase64: 'A'.repeat(MAX_DATA_BASE64_LENGTH) })).compatible + ).toBe(true) + expect( + readChunk(chunkReply({ dataBase64: 'A'.repeat(MAX_DATA_BASE64_LENGTH + 1) })).compatible + ).toBe(false) + }) + + it('requires every member that makes a chunk self-describing', () => { + for (const overrides of [ + { buildId: 'nope' }, + { path: '../escape.js' }, + { offset: -1 }, + { offset: 1.5 }, + { assetByteLength: MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES + 1 }, + { sha256: 'b'.repeat(63) }, + { dataBase64: undefined }, + { eof: 'yes' }, + { eof: undefined } + ]) { + expect(readChunk(chunkReply(overrides)).compatible).toBe(false) + } + }) + + it('refuses a reply that is not an object at all', () => { + for (const raw of [null, undefined, 'chunk', 7, []]) { + expect(readChunk(raw).compatible).toBe(false) + } + }) +}) + +describe('mobile web bundle error codes', () => { + it('maps every code the host declares, from both positions one can occupy', () => { + expect(MOBILE_WEB_BUNDLE_ERROR_CODES).toHaveLength(6) + for (const code of MOBILE_WEB_BUNDLE_ERROR_CODES) { + expect(readMobileWebBundleErrorCode(new Error(`invalid_argument: ${code}`))).toBe(code) + expect(readMobileWebBundleErrorCode(new Error(code))).toBe(code) + expect(readMobileWebBundleErrorCode(new Error(`invalid_argument: ${code}: no bundle`))).toBe( + code + ) + } + }) + + it('answers null for anything that is not one of those six', () => { + expect(readMobileWebBundleErrorCode(new Error('invalid_argument: some_other_code'))).toBeNull() + expect(readMobileWebBundleErrorCode(new Error('internal_error: boom'))).toBeNull() + expect(readMobileWebBundleErrorCode(new Error(' '))).toBeNull() + expect(readMobileWebBundleErrorCode(new Error('Network request failed'))).toBeNull() + expect(readMobileWebBundleErrorCode('mobile_web_bundle_unavailable')).toBeNull() + expect(readMobileWebBundleErrorCode(null)).toBeNull() + }) + + it('does not read a code a host merely quoted somewhere in its prose', () => { + expect( + readMobileWebBundleErrorCode( + new Error('invalid_argument: the bundle is mobile_web_bundle_unavailable here') + ) + ).toBeNull() + // Unanchored, this one reads as a refusal; the code is neither the whole message nor what + // follows the envelope code. + expect( + readMobileWebBundleErrorCode(new Error('RPC mobile_web_bundle_unavailable failed')) + ).toBeNull() + // The second position is `: `, exactly. Slicing the leading token's length off + // any message would make this one read as the code that follows the bracket. + expect( + readMobileWebBundleErrorCode(new Error('rpc (mobile_web_bundle_unavailable)')) + ).toBeNull() + }) + + it('reads a dispatcher schema refusal, whose message is prose, as no code at all', () => { + for (const message of [ + 'invalid_argument: Invalid input: expected string, received number', + 'invalid_argument: too_small: expected string to have >=1 characters' + ]) { + expect(readMobileWebBundleErrorCode(new Error(message))).toBeNull() + } + }) +}) + +describe('mobile web bundle operation descriptors', () => { + it('accepts no reply but a result', () => { + // A salvage or bare-message policy would hand the fetch a half-bundle that fails a hash check + // much later, far from the cause. + for (const operation of [mobileWebBundleManifestRead, mobileWebBundleChunkRead]) { + expect(operation.acceptance).toBe('require-result-or-throw') + expect(operation.barrier).toBe('on-settle') + } + }) +}) diff --git a/mobile/src/transport/mobile-web-bundle-reply-schemas.ts b/mobile/src/transport/mobile-web-bundle-reply-schemas.ts new file mode 100644 index 00000000000..aa1ba5ec662 --- /dev/null +++ b/mobile/src/transport/mobile-web-bundle-reply-schemas.ts @@ -0,0 +1,82 @@ +import { z } from 'zod' +import { MOBILE_WEB_BUNDLE_CHUNK_BYTES } from '../../../src/shared/mobile-web-bundle/bundle-rpc-contract' +import { + MobileWebBundleAssetPathSchema, + MOBILE_WEB_BUNDLE_MAX_ASSETS, + MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES, + MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES, + MOBILE_WEB_BUNDLE_SCHEMA_VERSION +} from '../../../src/shared/mobile-web-bundle/manifest-contract' + +// Hoisted, never built inside a reader: a schema constructed per parse cost 2275 ns against 156 ns +// for the same shape hoisted (#21311). +// +// Loose where the host contract is strict, and required only where this client reads. The host's +// own schemas describe what it produces and stay `.strict()`; a phone that rejected an unknown +// member would turn a later optional field into a released-client break instead of the Rule 1 +// addition `docs/reference/remote-wire-compatibility.md` allows. + +/** Lowercase hex digest. The shared contract keeps its copy private, so this is the one place the + * client states the shape it accepts. */ +const SHA256_PATTERN = /^[a-f0-9]{64}$/ + +/** Base64 of one chunk, bounded by the same arithmetic as `skill-upload-session-contract.ts`, so a + * host that overshoots is refused at the boundary instead of at reassembly. */ +const MAX_DATA_BASE64_LENGTH = Math.ceil(MOBILE_WEB_BUNDLE_CHUNK_BYTES / 3) * 4 + 8 + +const assetSchema = z.looseObject({ + path: MobileWebBundleAssetPathSchema, + sha256: z.string().regex(SHA256_PATTERN), + byteLength: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), + contentType: z.string().min(1) +}) + +/** Everything the fetch reads: the id it caches under, the assets it pages, and the entry it will + * later load. `desktopVersion` and the protocol window pass through untyped — Phase B's update + * wall reads them, this phase does not. + * + * `schemaVersion` stays a literal because the manifest is closed in both directions: a bump is the + * only change path, and an unrecognised one is an unusable bundle to re-fetch, never a crash. */ +const manifestSchema = z + .looseObject({ + schemaVersion: z.literal(MOBILE_WEB_BUNDLE_SCHEMA_VERSION), + buildId: z.string().regex(SHA256_PATTERN), + entrypoint: MobileWebBundleAssetPathSchema, + totalBytes: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES), + assets: z.array(assetSchema).min(1).max(MOBILE_WEB_BUNDLE_MAX_ASSETS) + }) + // The allocation bound, and the reason it is the sum rather than `totalBytes`: the fetch + // allocates one buffer per asset from `byteLength` and holds them all, so a manifest declaring + // `totalBytes` 0 alongside 256 assets of 10 MiB each would pass every ceiling above and still + // cost 2560 MiB. The host pins sum === totalBytes; this client never trusts `totalBytes` for + // anything, so it bounds what it will actually allocate instead. + .refine( + (manifest) => + manifest.assets.reduce((sum, asset) => sum + asset.byteLength, 0) <= + MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES, + 'assets sum to more than the contract total' + ) + +/** `chunkBytes` is read, never assumed: the host may shrink it without a client release. Capped at + * the constant because a larger value would overshoot `dataBase64` above. */ +export const MobileWebBundleManifestReplySchema = z.looseObject({ + manifest: manifestSchema, + chunkBytes: z.number().int().positive().max(MOBILE_WEB_BUNDLE_CHUNK_BYTES) +}) + +/** Self-describing on purpose: `buildId`, `path` and `offset` are echoed so a reassembler cannot + * misplace a reply, and `sha256`/`assetByteLength` describe the whole asset rather than this + * chunk, which is what lets the fetch verify without a second index. */ +export const MobileWebBundleChunkReplySchema = z.looseObject({ + buildId: z.string().regex(SHA256_PATTERN), + path: MobileWebBundleAssetPathSchema, + offset: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), + assetByteLength: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), + sha256: z.string().regex(SHA256_PATTERN), + dataBase64: z.string().max(MAX_DATA_BASE64_LENGTH), + eof: z.boolean() +}) + +export type MobileWebBundleManifestReply = z.output +export type MobileWebBundleManifestRead = MobileWebBundleManifestReply['manifest'] +export type MobileWebBundleAssetRead = MobileWebBundleManifestRead['assets'][number] diff --git a/mobile/src/transport/rpc-operation-compile-fence.ts b/mobile/src/transport/rpc-operation-compile-fence.ts index ac14b326fd2..c57c4950a01 100644 --- a/mobile/src/transport/rpc-operation-compile-fence.ts +++ b/mobile/src/transport/rpc-operation-compile-fence.ts @@ -9,6 +9,7 @@ import { } from './rpc-operation' import { rpcResultVariants } from './rpc-operation-result-reader' import { + pushTestWithoutParams, workspaceListAtBarrier, workspaceListOrNull, workspaceRowsReader, @@ -146,6 +147,17 @@ export async function fenceBarrierAndParams(): Promise { ) } +// A method the catalog declares params-less keeps every shape a shipped sender may use. An +// explicit `null` is the one that matters: `params: null` is not the frame that omits the key, +// so narrowing this to omission would rewrite bytes main already puts on the wire. +export async function fenceParamlessSend(): Promise { + await runRpcOperation(client, pushTestWithoutParams, null) + await runRpcOperation(client, pushTestWithoutParams, undefined) + await runRpcOperation(client, pushTestWithoutParams) + // @ts-expect-error a method that declares no params accepts none + await runRpcOperation(client, pushTestWithoutParams, { path: 'main.js' }) +} + export async function fenceVerdictTypes(): Promise { // @ts-expect-error the probe's policy yields a boolean, not the other family's rows const rows: WorkspaceRows = await runRpcOperation(client, worktreePsProbe, {}) diff --git a/mobile/src/transport/rpc-operation-test-families.ts b/mobile/src/transport/rpc-operation-test-families.ts index 73554b7a4f8..a93c0ba1abf 100644 --- a/mobile/src/transport/rpc-operation-test-families.ts +++ b/mobile/src/transport/rpc-operation-test-families.ts @@ -54,6 +54,14 @@ export const worktreePsProbe = defineRpcOperation({ barrier: 'on-settle' }) +/** A method the catalog declares with no params at all, so `RpcSendParams` reads `void`. */ +export const pushTestWithoutParams = defineRpcOperation({ + name: 'test.pushTestWithoutParams', + method: 'notifications.testPush', + acceptance: 'method-not-found-refusal', + barrier: 'on-settle' +}) + export const terminalStreamOpener = defineRpcOperation({ name: 'test.terminalStreamOpener', method: 'terminal.subscribe', diff --git a/mobile/src/transport/rpc-operation.ts b/mobile/src/transport/rpc-operation.ts index a836dc429fb..93424f35433 100644 --- a/mobile/src/transport/rpc-operation.ts +++ b/mobile/src/transport/rpc-operation.ts @@ -173,10 +173,13 @@ export async function runRpcOperation< >( client: UnvalidatedRpcRequestPort, operation: RpcOperation, - params: RpcSendParams, - options?: SendRequestOptions + // Shares the deferred sender's tuple so the two cannot disagree about what a params-less + // method may be called with: the catalog types those `void`, and an explicit `null` is the + // frame three of them go out with today (`notifications.testPush`, + // `notifications.unregisterPush`, `speech.models.list`), all through the deferred entry point. + ...args: RpcSendArguments ): Promise> { - const outcome = await request(client, operation, params, options) + const outcome = await request(client, operation, args[0], args[1]) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. return interpretRpcOutcome(operation, outcome) as RpcVerdict } diff --git a/mobile/src/transport/settings-read-operations.test.ts b/mobile/src/transport/settings-read-operations.test.ts index 07f4ff771d4..87243dee245 100644 --- a/mobile/src/transport/settings-read-operations.test.ts +++ b/mobile/src/transport/settings-read-operations.test.ts @@ -217,3 +217,27 @@ describe('new-tab settlement barriers', () => { expect(() => readSettings()).toThrow(TypeError) }) }) + +describe('the bound descriptor', () => { + // Eleven call sites pass `interpret` detached from its descriptor, five of them + // settlePreviewSend's second argument in files/mobile-file-preview-request.ts: + // filePreviewTextRead, filePreviewImageRead, terminalArtifactTextRead, + // terminalArtifactImageRead and terminalArtifactWrite. + it('interprets the same reply when taken as an unbound reference', async () => { + const settings = { futureField: 'kept' } + const accepted = await settingsRead.request(replyWith(success({ settings }))) + const readSettings = settingsRead.interpret + expect(readSettings(accepted)).toEqual(settingsRead.interpret(accepted)) + expect(readSettings(accepted)).toEqual({ accepted: true, value: settings }) + + const refused = await botOverridesRead.request(replyWith(refusal())) + const readOverrides = botOverridesRead.interpret + expect(readOverrides(refused)).toEqual(botOverridesRead.interpret(refused)) + expect(readOverrides(refused)).toEqual({ accepted: false }) + + // The throwing acceptance family keeps its throw unbound rather than losing it. + const missing = await newTabSettingsRead.request(replyWith(success(null))) + const readNewTab = newTabSettingsRead.interpret + expect(() => readNewTab(missing)()).toThrow(TypeError) + }) +}) diff --git a/mobile/src/transport/stable-logical-rpc-client.test.ts b/mobile/src/transport/stable-logical-rpc-client.test.ts index 6a22b3654a2..ca599f32373 100644 --- a/mobile/src/transport/stable-logical-rpc-client.test.ts +++ b/mobile/src/transport/stable-logical-rpc-client.test.ts @@ -456,3 +456,50 @@ describe('stable logical RPC client', () => { expect(client.getGeneration()).toBe(1) }) }) + +describe('stable logical RPC client subscription fencing', () => { + /** A physical session with an inert disposer, so only the logical guard can fence a late event. */ + function leakySession() { + const session = new FakeSession('connected') + const listeners = new Set<(result: unknown) => void>() + session.subscribe.mockImplementation((_method, _params, listener) => { + listeners.add(listener) + return () => {} + }) + return { + session, + emit(value: unknown): void { + for (const listener of listeners) { + listener(value) + } + } + } + } + + it('delivers a stream event to a live subscription', () => { + const physical = leakySession() + const client = createStableLogicalRpcClient(physical.session, 'lan') + const listener = vi.fn() + client.subscribe('notifications.subscribe', { includeDesktopSuppressed: true }, listener) + + physical.emit({ type: 'ready', subscriptionId: 'sub-1' }) + + expect(listener).toHaveBeenCalledExactlyOnceWith({ type: 'ready', subscriptionId: 'sub-1' }) + }) + + it('drops a stream event that lands after the caller unsubscribed', () => { + const physical = leakySession() + const client = createStableLogicalRpcClient(physical.session, 'lan') + const listener = vi.fn() + const unsubscribe = client.subscribe( + 'notifications.subscribe', + { includeDesktopSuppressed: true }, + listener + ) + + unsubscribe() + physical.emit({ type: 'ready', subscriptionId: 'sub-1' }) + + expect(listener).not.toHaveBeenCalled() + }) +}) diff --git a/mobile/src/transport/unchecked-rpc-reader-boundary.test.ts b/mobile/src/transport/unchecked-rpc-reader-boundary.test.ts index da07d8dd9c1..efdc426cabf 100644 --- a/mobile/src/transport/unchecked-rpc-reader-boundary.test.ts +++ b/mobile/src/transport/unchecked-rpc-reader-boundary.test.ts @@ -130,11 +130,9 @@ describe('unchecked RPC reader boundary', () => { }) it('scans a plausible number of files', () => { - // A broken root or extension filter would make every check below vacuously pass. The file floor - // is safe to hold at a constant; an offender-count floor is not, because the list counts down to - // zero. Main's batch took it from 29 files to 16 and its floor from 20 to 10; this batch reaches - // 8, below that floor. Against the list instead, the check survives every step of the countdown: - // a filter that scanned nothing reports 0 against a list naming 8. + // A broken root or extension filter would make every check below vacuously pass. The list has + // reached zero, so the equality now asserts "no unchecked reader ships" — which a scan of + // nothing would also satisfy. The file floor is what rules that out, and it stays a constant. expect(scanned.length).toBeGreaterThan(400) expect(observed.size).toBe(inventory.length) }) diff --git a/mobile/src/transport/unchecked-rpc-reader-inventory.ts b/mobile/src/transport/unchecked-rpc-reader-inventory.ts index edcc1417694..44ff9878ab4 100644 --- a/mobile/src/transport/unchecked-rpc-reader-inventory.ts +++ b/mobile/src/transport/unchecked-rpc-reader-inventory.ts @@ -8,20 +8,11 @@ * somewhere downstream — a property read on null, a `.map` on a string, a rendered `undefined` — * with nothing naming the reply as the cause. * - * The count is per file and is a ceiling, not a target: unchecked-rpc-reader-boundary.test.ts fails - * on a file that is not listed, on a listed file that no longer has one, and on a listed file whose - * count went up. Replacing a reader with `rpcResultVariant(variant, schema)` lowers its line; the - * list only shrinks. - * - * A merge is the one case where a line goes up without a migration undoing itself: main can land an - * operation the branch never saw. Raise the line then, and name the PR that brought it, so the next - * reader can tell an import from a regression. Of the three #20954 brought, - * `native-chat-session-page` is still here; `notification-stream-closed` and - * `terminal-buffer-cleared` were converted by the notifications/terminal batch. - * - * A file leaves the list by deletion, not by reaching zero: an entry asserts the file still holds - * at least one unchecked reader, so a `readers: 0` line is itself a failure. Migrating a domain - * therefore removes its files outright. + * The list is empty. Step 7 converted the last domains it named, so the countdown is over and the + * ratchet has flipped direction: unchecked-rpc-reader-boundary.test.ts now fails on the first + * unchecked reader anywhere under `app/` or `src/`, and nothing may be added back. That includes a + * merge bringing an operation this branch never saw — convert it with + * `rpcResultVariant(variant, schema)` in the merge rather than reopening a line here. * * Two holes this list does not close, both deliberate: * - A hand-written reader that returns `{ compatible: true, ... }` without going through those @@ -38,21 +29,7 @@ export type UncheckedRpcReaderEntry = { /** * Files holding at least one unchecked reader, grouped by the feature area that owns them. * - * The reason is shared by every line and is stated once here instead of 37 times: the reply has no - * schema, so the operation declares what the payload is by assertion. Writing one schema per - * consumed member — required exactly where the consumer reads it unguarded, optional everywhere - * else, never `.strict()` — turns the assertion into a check and deletes the line. + * Empty, and the entry shape outlives it: the boundary test measures the scan against this list, so + * an empty list is what makes "no unchecked reader ships" an assertion rather than a claim. */ -export const UNCHECKED_RPC_READERS: readonly UncheckedRpcReaderEntry[] = [ - // agent-history - { file: 'src/agent-history/mobile-agent-history-operations.ts', readers: 6 }, - // dictation - { file: 'src/dictation/mobile-dictation-operations.ts', readers: 8 }, - // files - { file: 'src/files/mobile-file-explorer-operations.ts', readers: 2 }, - { file: 'src/files/mobile-file-ownership-operations.ts', readers: 2 }, - { file: 'src/files/mobile-file-preview-operations.ts', readers: 6 }, - { file: 'src/files/mobile-file-tab-doc-operations.ts', readers: 3 }, - // host-screen - { file: 'src/host-screen/host-screen-operations.ts', readers: 8 } -] +export const UNCHECKED_RPC_READERS: readonly UncheckedRpcReaderEntry[] = [] diff --git a/mobile/src/worktree/workspace-view-settings.test.ts b/mobile/src/worktree/workspace-view-settings.test.ts index 81ea12621c4..4bcdfecf83b 100644 --- a/mobile/src/worktree/workspace-view-settings.test.ts +++ b/mobile/src/worktree/workspace-view-settings.test.ts @@ -15,6 +15,7 @@ const base: MobileViewState = { sortMode: 'recent', hideSleeping: false, hideDefaultBranch: false, + alwaysShowDefaultBranch: false, filterRepoIds: [], collapsedGroups: [], workspaceStatuses: DEFAULT_MOBILE_WORKSPACE_STATUSES diff --git a/mobile/src/worktree/worktree-host-context-labels.ts b/mobile/src/worktree/worktree-host-context-labels.ts index de33c62ca5d..406e6d8ce03 100644 --- a/mobile/src/worktree/worktree-host-context-labels.ts +++ b/mobile/src/worktree/worktree-host-context-labels.ts @@ -10,7 +10,6 @@ export { buildHostLabelById, getHostContextLabel } from '../../../src/shared/worktree/host-context-labels' -import type { RepoSummary } from './host-worktree-rpc-types' import type { Worktree } from './workspace-list-types' export type HostLabelSources = { @@ -23,7 +22,7 @@ export type HostLabelSources = { } export function buildRepoHostIdByRepoId( - repos: readonly Pick[] + repos: readonly { id: string; connectionId?: string | null; executionHostId?: string | null }[] ): Map { return new Map(repos.map((repo) => [repo.id, getRepoExecutionHostId(repo)])) } diff --git a/mobile/tests-typecheck-baseline.txt b/mobile/tests-typecheck-baseline.txt new file mode 100644 index 00000000000..0e2c3b3c0f7 --- /dev/null +++ b/mobile/tests-typecheck-baseline.txt @@ -0,0 +1,131 @@ +# Test files that do NOT yet typecheck under mobile/tsconfig.test.json. +# This is a RATCHET: the list may only SHRINK. Do NOT add entries to get CI green — +# an unchecked test is one whose type-level pins prove nothing. +# Regenerate/prune: node scripts/check-tests-typecheck-ratchet.mjs --prune +src/accounts-route-reset-credit.test.ts +src/browser/browser-command-reply-schema.test.ts +src/browser/mobile-browser-frameless-stream.test.tsx +src/components/CodexResetCreditAction.test.ts +src/components/HostProtocolGate.test.ts +src/components/MobileHomeQuickActions.test.ts +src/components/MobileHostCard-truthful-status.test.tsx +src/components/MobileHostCard.test.ts +src/components/MobileMarkdown.file-links.test.ts +src/components/MobileRichMarkdownEditor.test.tsx +src/components/NewWorktreeModal.test.tsx +src/components/PickerModal.accessibility.test.ts +src/components/WorktreeAgentList.test.tsx +src/components/agent-monitoring-indicators.test.ts +src/components/bottom-drawer-close-lifecycle.test.ts +src/components/bottom-drawer-window-handback.test.ts +src/components/codex-reset-credit-capability.test.ts +src/components/mobile-agent-icon-gradient.test.ts +src/components/pr-sidebar/PRCommentsSection.test.ts +src/diagnostics/connection-diagnostics-submission.test.ts +src/dictation/mobile-dictation-setup.test.ts +src/files/MobileFileExplorerPanel.test.ts +src/files/MobileFileMarkdownPreview.test.ts +src/hooks/mobile-dictation-desktop-start.test.ts +src/host-edit-route-accessibility.test.ts +src/host-edit-save-flow.test.ts +src/mock-server-key-pair.test.ts +src/notifications/notification-consent-ownership.test.ts +src/notifications/push-dismissal-native-races.test.ts +src/notifications/push-receive.test.ts +src/notifications/push-registration.test.ts +src/notifications/push-token.test.ts +src/onboarding/MobileOnboardingPage.test.ts +src/onboarding/NotificationOnboardingPreview.test.ts +src/onboarding/legacy-notification-opt-in-route.test.ts +src/onboarding/mobile-onboarding-screen.test.ts +src/session/MobileMarkdownReader.test.tsx +src/session/MobileNativeChatComposer.test.ts +src/session/MobileNativeChatMessage.test.ts +src/session/MobileNativeChatOverlay.test.ts +src/session/MobileNativeChatPermission.test.ts +src/session/MobileNativeChatQuestion.test.tsx +src/session/MobileNativeChatSessionOptionPickers.test.ts +src/session/MobileNativeChatSessionOptionRows.test.ts +src/session/MobileNativeChatTurnStatus.test.ts +src/session/MobileNativeChatView.test.ts +src/session/QuickCommandsList.test.ts +src/session/QuickCommandsSheet.test.ts +src/session/ai-vault-resume-launch.test.ts +src/session/mobile-file-tap-open.test.ts +src/session/mobile-native-chat-open-file.test.ts +src/session/mobile-session-route-parity.test.ts +src/session/mobile-session-tab-activation.test.ts +src/session/mobile-worker-takeover-send-sites.test.ts +src/session/pending-terminal-handle-recovery-poll.test.ts +src/session/session-reply-schema.test.ts +src/session/use-mobile-diff-review-send-actions.test.ts +src/session/use-mobile-file-tap-handlers.test.ts +src/session/use-mobile-native-chat-image-attachments.test.ts +src/session/use-mobile-native-chat-turn-disclosure.test.tsx +src/session/use-mobile-pr-branch-context.test.ts +src/session/use-mobile-pr-sidebar-controller.test.ts +src/session/use-mobile-session-terminal-create-actions.test.ts +src/session/use-mobile-structured-agent-session-send.test.tsx +src/session/use-mobile-structured-agent-session.test.tsx +src/session/use-mobile-structured-prompt-responses.test.tsx +src/session/use-mobile-terminal-inventory-recovery.test.ts +src/settings/native-notification-delivery-settings.test.tsx +src/settings/notification-display-test.test.tsx +src/settings/settings-screen-state.test.tsx +src/settings/voice-settings-poller-refresh.test.tsx +src/source-control/MobileGitHistoryList.test.tsx +src/source-control/mobile-branch-compare.test.ts +src/source-control/mobile-create-pr-action.test.ts +src/source-control/mobile-git-status.test.ts +src/source-control/mobile-hosted-review-create-intent-runner.test.ts +src/source-control/use-mobile-hosted-review-eligibility.test.ts +src/tasks/source-workspace-create.test.ts +src/tasks/task-source-search-reply-schema.test.ts +src/terminal/buffered-terminal-draft-restoration.test.ts +src/terminal/mobile-terminal-query-reply.test.ts +src/terminal/terminal-caret-rendering-oracle.test.ts +src/terminal/terminal-live-accessory-raw-send.test.ts +src/terminal/terminal-live-text-commit.test.ts +src/terminal/terminal-webview-engine-error.test.ts +src/terminal/terminal-webview-query-reply-routing.test.ts +src/terminal/terminal-webview-wheel-scroll.test.ts +src/terminal/use-buffered-terminal-drafts.test.tsx +src/terminal/worker-terminal-takeover-report.test.ts +src/test-support/rpc-recording/native-mounting-substitutes.test.ts +src/test-support/rpc-recording/operation-module-loader.test.ts +src/test-support/rpc-recording/screen-native-substitutes.test.ts +src/transport/client-context.test.ts +src/transport/connection-revival-triggers.test.ts +src/transport/host-credential-cleanup.test.ts +src/transport/host-edit-navigation.test.ts +src/transport/host-list-load-sharing.test.ts +src/transport/host-removal-lifecycle.test.ts +src/transport/host-status-gates.test.ts +src/transport/host-store.test.ts +src/transport/mobile-endpoint-supervisor-nudge.test.ts +src/transport/mobile-relay-background-grace.test.ts +src/transport/mobile-relay-background-lifecycle.test.ts +src/transport/mobile-relay-direct-upgrade.test.ts +src/transport/mobile-relay-e2ee-link.test.ts +src/transport/mobile-relay-pairing-recovery.test.ts +src/transport/mobile-relay-pairing-reply.test.ts +src/transport/mobile-relay-physical-client.test.ts +src/transport/mobile-relay-reconnect-controller.test.ts +src/transport/mobile-relay-resume-director.test.ts +src/transport/mobile-relay-rpc-session-liveness.test.ts +src/transport/mobile-relay-rpc-session.test.ts +src/transport/mobile-relay-runtime-failover.test.ts +src/transport/pairing-relay-candidate.test.ts +src/transport/pre-profile-pairing-coordinator.test.ts +src/transport/relay-host-signed-out-verdict.test.ts +src/transport/rpc-client-synthesized-close-diagnostics.test.ts +src/transport/rpc-operation.test.ts +src/transport/rpc-session-liveness-watchdog.test.ts +src/transport/runtime-capability-probe.test.ts +src/transport/settings-host-client-lifecycle.test.ts +src/worktree/agent-row-display.test.ts +src/worktree/agent-row-lineage.test.ts +src/worktree/host-worktree-refresh.test.ts +src/worktree/worktree-host-row-identity.test.ts +src/worktree/worktree-list-completeness.test.ts +src/worktree/worktree-list-snapshot.test.ts diff --git a/mobile/tsconfig.test.json b/mobile/tsconfig.test.json new file mode 100644 index 00000000000..b6bb998837f --- /dev/null +++ b/mobile/tsconfig.test.json @@ -0,0 +1,18 @@ +{ + // The main config excludes test files so Metro never compiles them into the release bundle, and + // vitest transpiles without typechecking — so until this config existed nothing typechecked a + // mobile test, and a type-level pin written in one proved nothing. + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + // These four are Node-side, not React Native: they import the desktop main process or + // src/shared/child-process, whose modules are written against @types/node. This program's libs + // are React Native's, where `setTimeout` answers a number rather than a NodeJS.Timeout, so + // pulling that graph in reports ~280 errors about the desktop rather than about mobile. Their + // own runtime coverage is vitest, which runs them under Node. + "scripts/rpc-recording-pin-guard.test.ts", + "src/tasks/agent-launch-mobile-replay.test.ts", + "src/tasks/mobile-agent-launch-architecture.test.ts", + "src/transport/mobile-relay-browser-cancel-budget.test.ts" + ] +} diff --git a/package.json b/package.json index 9d37cb13cba..23d1ecf3e34 100644 --- a/package.json +++ b/package.json @@ -93,10 +93,11 @@ "build:electron-vite:parallel": "node config/scripts/run-electron-vite-targets-in-parallel.mjs", "build:web": "node config/scripts/run-vite-web-build.mjs && node config/scripts/verify-web-build.mjs", "build:web-from-renderer": "node config/scripts/project-renderer-web-client.mjs && node config/scripts/verify-web-build.mjs", - "build:desktop": "pnpm run typecheck && pnpm run build:relay && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer", + "build:mobile-web": "node config/scripts/build-mobile-web-bundle.mjs && node config/scripts/verify-mobile-web-bundle.mjs", + "build:desktop": "pnpm run typecheck && pnpm run build:relay && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "build": "pnpm run build:desktop && pnpm run build:native", - "build:release": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer", - "build:release:parallel": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer", + "build:release": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", + "build:release:parallel": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "postinstall": "node config/scripts/rebuild-native-deps.mjs", "rebuild:electron": "node config/scripts/rebuild-native-deps.mjs", "reclaim:electron-dists": "node config/scripts/reclaim-electron-dists.mjs", 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/agent-launch/agent-launch-executor.test.ts b/src/main/agent-launch/agent-launch-executor.test.ts index 8910ccd1e60..eb71b45537d 100644 --- a/src/main/agent-launch/agent-launch-executor.test.ts +++ b/src/main/agent-launch/agent-launch-executor.test.ts @@ -15,6 +15,7 @@ import { type AgentLaunchExecution } from './agent-launch-executor' import type { AgentLaunchIntent } from '../../shared/agent-launch-intent' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' const STRUCTURED_PREFERENCE = { experimentalNativeChat: true, @@ -27,6 +28,7 @@ function harness(options: { createSupport?: { supported: boolean; reason?: 'agent' | 'remote' | 'wsl' } createSupportThrows?: boolean structuredCreateError?: Error + deliveredMessageId?: string | null }) { const calls: string[] = [] const createWorktree = vi.fn( @@ -50,12 +52,16 @@ function harness(options: { if (options.structuredCreateError) { throw options.structuredCreateError } - return { sessionId: 'sess-1', handle: 'handle_structured' } + return { sessionId: 'sess-1', handle: 'handle_structured', fence: 4 } }) const createTerminalAgent = vi.fn(async () => { calls.push('createTerminalAgent') return { handle: 'term_1' } }) + const deliverStructuredPrompt = vi.fn(async () => { + calls.push('deliverStructuredPrompt') + return options.deliveredMessageId === undefined ? 'msg-1' : options.deliveredMessageId + }) const runtime = { getClientSettings: () => options.settings === undefined ? STRUCTURED_PREFERENCE : options.settings, @@ -66,12 +72,13 @@ function harness(options: { createWorktree, createStructuredSession, createTerminalAgent, + deliverStructuredPrompt, run: (intent: AgentLaunchIntent) => executeAgentLaunch({ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub implements only the two runtime methods the executor reaches, and each test asserts the calls made, so an omitted method throws rather than reading a wrong value. runtime: runtime as unknown as AgentLaunchExecution['runtime'], intent, - surfaces: { createStructuredSession, createTerminalAgent }, + surfaces: { createStructuredSession, createTerminalAgent, deliverStructuredPrompt }, workspaces: { createWorktree } }) } @@ -232,14 +239,63 @@ describe('an agent with no structured session', () => { }) describe('the prompt receipt', () => { - it('reports a requested prompt as not delivered rather than omitting it', async () => { + const SUBMIT = { text: 'do the thing', delivery: 'submit' } as const + + it('commits a submitted prompt to the session the launch created and names the row', async () => { + const h = harness({}) + const result = await h.run({ ...CREATE_INTENT, prompt: SUBMIT }) + + expect(result.prompt).toEqual({ delivery: 'submit', outcome: 'journaled', messageId: 'msg-1' }) + // Delivery is sequenced after the surface exists; there is nothing to send into before that. + expect(h.calls).toEqual([ + 'createWorktree(startupAgent=undefined)', + 'createSupport', + 'createStructuredSession', + 'deliverStructuredPrompt' + ]) + // The send must name the lease the create was admitted under, not one re-read later. + expect(h.deliverStructuredPrompt).toHaveBeenCalledWith({ + sessionId: 'sess-1', + fence: 4, + prompt: SUBMIT + }) + }) + + it('under-claims as not delivered when nothing was committed', async () => { + const h = harness({ deliveredMessageId: null }) + const result = await h.run({ ...CREATE_INTENT, prompt: SUBMIT }) + // A resend costs a duplicate; claiming a row that does not exist loses the text silently. + expect(result.prompt).toEqual({ delivery: 'submit', outcome: 'not-delivered' }) + }) + + it('leaves a draft with the caller, because the host has no composer to hold one', async () => { const h = harness({}) const result = await h.run({ ...CREATE_INTENT, prompt: { text: 'do the thing', delivery: 'draft' } }) - // The executor delivers nothing, so the only honest outcome is the one that under-claims. expect(result.prompt).toEqual({ delivery: 'draft', outcome: 'not-delivered' }) + expect(h.deliverStructuredPrompt).not.toHaveBeenCalled() + }) + + it('leaves a terminal launch to the pane owner', async () => { + const h = harness({ createSupport: { supported: false, reason: 'wsl' } }) + const result = await h.run({ ...CREATE_INTENT, prompt: SUBMIT }) + expect(result.outcome.kind).toBe('terminal') + expect(result.prompt).toEqual({ delivery: 'submit', outcome: 'not-delivered' }) + expect(h.deliverStructuredPrompt).not.toHaveBeenCalled() + }) + + it('leaves a reused terminal to the pane owner', async () => { + const h = harness({}) + const result = await h.run({ + agent: 'claude', + target: { kind: 'existing', worktree: 'wt-7' }, + reuseTerminal: { handle: 'term_existing' }, + prompt: SUBMIT + }) + expect(result.prompt).toEqual({ delivery: 'submit', outcome: 'not-delivered' }) + expect(h.deliverStructuredPrompt).not.toHaveBeenCalled() }) it('omits the receipt when no prompt was requested', async () => { @@ -247,3 +303,50 @@ describe('the prompt receipt', () => { expect((await h.run(CREATE_INTENT)).prompt).toBeUndefined() }) }) + +/** + * The kind is read off the resolved workspace id, so a workspace with nowhere to keep a session is + * decided here rather than offered to a host probe that cannot answer for it. + */ +describe('a launch into an existing workspace, by workspace kind', () => { + it('runs the floating workspace as a terminal, never a structured session', async () => { + const h = harness({}) + const result = await h.run({ + agent: 'claude', + target: { kind: 'existing', worktree: FLOATING_TERMINAL_WORKTREE_ID } + }) + + // The invariant, not the call order: the floating sentinel has no session store to open into. + expect(h.createStructuredSession).not.toHaveBeenCalled() + expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_1' }) + expect(result.receipt).toMatchObject({ + mode: 'terminal', + reason: 'structured_unsupported_on_host' + }) + }) + + it('still opens a structured session in a folder workspace', async () => { + const h = harness({}) + const result = await h.run({ + agent: 'claude', + target: { kind: 'existing', worktree: 'folder:fw-1' } + }) + + // A folder workspace has no git worktree either; it must not be swept up with the sentinel. + expect(h.createTerminalAgent).not.toHaveBeenCalled() + expect(result.outcome).toEqual({ + kind: 'structured', + sessionId: 'sess-1', + handle: 'handle_structured' + }) + expect(result.receipt).toMatchObject({ mode: 'structured' }) + }) + + it('still opens a structured session in a git worktree', async () => { + const h = harness({}) + const result = await h.run({ agent: 'claude', target: { kind: 'existing', worktree: 'wt-7' } }) + + expect(result.outcome.kind).toBe('structured') + expect(result.receipt).toMatchObject({ mode: 'structured' }) + }) +}) diff --git a/src/main/agent-launch/agent-launch-executor.ts b/src/main/agent-launch/agent-launch-executor.ts index c65f63061fa..91cf75c0fad 100644 --- a/src/main/agent-launch/agent-launch-executor.ts +++ b/src/main/agent-launch/agent-launch-executor.ts @@ -27,11 +27,16 @@ import type { AgentLaunchIntent, + AgentLaunchPrompt, AgentLaunchResult, AgentLaunchTarget } from '../../shared/agent-launch-intent' import { withoutReservedAgentCreateFields } from '../../shared/agent-launch-intent' import type { TuiAgent } from '../../shared/tui-agent' +import { + workspaceKindForWorktreeId, + type WorkspaceLaunchKind +} from '../../shared/workspace-launch-kind' import type { OrcaRuntimeService } from '../runtime/orca-runtime' import { isDefinitiveAgentSessionCreateRefusal } from '../../shared/agent-session-definitive-refusal' import { @@ -51,12 +56,32 @@ export type AgentLaunchSurfaceFactory = { worktreeId: string agent: 'claude' | 'codex' options?: Readonly> - }): Promise<{ sessionId: string; handle: string }> + }): Promise createTerminalAgent(args: { worktreeId: string agent: TuiAgent options?: Readonly> }): Promise<{ handle: string; warning?: string }> + /** + * Commits the launch text as the session's first turn, answering with the transcript row's id. + * + * `null` means nothing was committed, and is the answer for every failure — a refused send, an + * unreachable host, a throw. Delivery must not fail a launch whose agent is already running: the + * caller can resend under `not-delivered`, but it cannot un-create a workspace. + */ + deliverStructuredPrompt?(args: { + sessionId: string + fence: number + prompt: AgentLaunchPrompt + }): Promise +} + +/** `fence` is carried out of the create because a send must name the lease it was admitted against, + * and re-reading it later would read whatever fence the session has by then. */ +export type AgentLaunchStructuredSurface = { + sessionId: string + handle: string + fence: number } /** A structured create refusal that proves no session was committed, so the launch may downgrade. */ @@ -107,6 +132,7 @@ export async function executeAgentLaunch( const preflight = decideAgentLaunchMode({ placement: { agent: intent.agent, + workspaceKind: launchWorkspaceKind(intent.target), ...(intent.reuseTerminal ? { terminal: intent.reuseTerminal.handle } : {}) }, settings, @@ -119,7 +145,7 @@ export async function executeAgentLaunch( outcome: { kind: 'terminal', handle: intent.reuseTerminal.handle }, worktreeId: existingWorktreeId(intent.target), receipt: preflight, - ...promptReceipt(intent) + ...promptReceipt(intent, null) } } @@ -131,7 +157,7 @@ export async function executeAgentLaunch( worktreeId: placed.worktreeId, receipt: preflight, ...(placed.warning ? { warning: placed.warning } : {}), - ...promptReceipt(intent) + ...promptReceipt(intent, null) } } @@ -145,7 +171,7 @@ export async function executeAgentLaunch( ) execution.onStage?.('surface_create') - let created: { outcome: AgentLaunchResult['outcome']; warning?: string } + let created: CreatedSurface try { created = await createSurface(execution, placed.worktreeId, settled) } catch (error) { @@ -187,10 +213,32 @@ export async function executeAgentLaunch( worktreeId: placed.worktreeId, receipt: settled, ...(warning ? { warning } : {}), - ...promptReceipt(intent) + ...promptReceipt(intent, await deliverLaunchPrompt(execution, created.structured)) } } +/** + * Hands the launch text to the surface that can commit it, which is a structured session and only + * a structured session: a terminal's paste is observed by whoever owns the pane, and a `draft` has + * no host-side home — the composer holds one, and the host has no composer. + */ +async function deliverLaunchPrompt( + execution: AgentLaunchExecution, + structured: AgentLaunchStructuredSurface | undefined +): Promise { + const { intent, surfaces } = execution + if (!intent.prompt || intent.prompt.delivery !== 'submit' || !structured) { + return null + } + return ( + (await surfaces.deliverStructuredPrompt?.({ + sessionId: structured.sessionId, + fence: structured.fence, + prompt: intent.prompt + })) ?? null + ) +} + function downgradeAgentLaunchModeForStructuredRefusal( receipt: AgentLaunchModeReceipt, vocabulary: AgentLaunchModeVocabulary @@ -229,11 +277,19 @@ async function resolveWorkspace( }) } +/** `structured` is the same surface `outcome` names, kept typed so prompt delivery reads the create's + * own fence rather than branching on `outcome.kind` and re-deriving it. */ +type CreatedSurface = { + outcome: AgentLaunchResult['outcome'] + warning?: string + structured?: AgentLaunchStructuredSurface +} + async function createSurface( execution: AgentLaunchExecution, worktreeId: string, settled: AgentLaunchModeReceipt -): Promise<{ outcome: AgentLaunchResult['outcome']; warning?: string }> { +): Promise { const { intent, surfaces } = execution if (settled.mode === 'structured' && isStructuredProvider(intent.agent)) { const session = await surfaces.createStructuredSession({ @@ -241,7 +297,10 @@ async function createSurface( agent: intent.agent, ...(intent.sessionOptions ? { options: intent.sessionOptions } : {}) }) - return { outcome: { kind: 'structured', sessionId: session.sessionId, handle: session.handle } } + return { + outcome: { kind: 'structured', sessionId: session.sessionId, handle: session.handle }, + structured: session + } } const terminal = await surfaces.createTerminalAgent({ worktreeId, @@ -279,12 +338,35 @@ function existingWorktreeId(target: AgentLaunchTarget): string { return target.kind === 'existing' ? target.worktree : '' } -/** Prompt delivery is the caller's, not the executor's: a PTY paste is observed by whoever owns - * the pane, and a structured first turn is sent through the session. The executor reports the - * requested delivery back as not delivered so a caller cannot mistake silence for delivery. */ -function promptReceipt(intent: AgentLaunchIntent): Pick { +/** + * Read from the id rather than carried alongside it, so the kind cannot disagree with the workspace + * it describes. `worktree` here is never a caller's selector — the method resolved it to an id + * before building the intent — and a create always produces a git worktree. + */ +function launchWorkspaceKind(target: AgentLaunchTarget): WorkspaceLaunchKind { + return target.kind === 'existing' ? workspaceKindForWorktreeId(target.worktree) : 'git-worktree' +} + +/** + * The one place a disposal is constructed, so the three arms cannot drift apart. + * + * `journaled` is reachable only from a committed message id, and that id exists only because the + * host appended the transcript row first — the receipt is a consequence of the commit, never a + * write-ahead of it. Everything else under-claims as `not-delivered`, which costs a resend; there + * is deliberately no arm for "maybe", because a caller holding one could neither resend nor drop + * the text. Dispatch doubt is not this tier's to report: the submission row carries it. + */ +function promptReceipt( + intent: AgentLaunchIntent, + messageId: string | null +): Pick { if (!intent.prompt) { return {} } - return { prompt: { delivery: intent.prompt.delivery, outcome: 'not-delivered' } } + const delivery = intent.prompt.delivery + return { + prompt: messageId + ? { delivery, outcome: 'journaled', messageId } + : { delivery, outcome: 'not-delivered' } + } } diff --git a/src/main/agent-launch/agent-launch-mode.ts b/src/main/agent-launch/agent-launch-mode.ts index 7533c0f2a01..1aee0e65cc3 100644 --- a/src/main/agent-launch/agent-launch-mode.ts +++ b/src/main/agent-launch/agent-launch-mode.ts @@ -32,6 +32,7 @@ import { } from '../../shared/structured-native-chat-launch-route' import type { TuiAgent } from '../../shared/tui-agent' import { hasExplicitTuiLaunchCommand } from '../../shared/tui-agent-launch-command-override' +import type { WorkspaceLaunchKind } from '../../shared/workspace-launch-kind' import type { OrcaRuntimeService } from '../runtime/orca-runtime' // The receipt is part of the launch contract, so it is declared with the rest of it; re-exported @@ -67,6 +68,10 @@ export type AgentLaunchModePlacement = { on?: string /** An existing terminal being reused. */ terminal?: string + /** Which kind of workspace the launch lands in, derived by the host from the workspace it + * resolved — never accepted from a caller, which would let one route around this decision. + * Absent means the kind was never established, and is not read as any particular kind. */ + workspaceKind?: WorkspaceLaunchKind } const DOWNGRADE_DETAIL: Record, string> = { @@ -131,9 +136,11 @@ export function decideAgentLaunchMode(args: { executionHostId: placement.on ? `runtime:${placement.on}` : 'local', reusesTerminal: Boolean(placement.terminal), hostCapabilities: RUNTIME_CAPABILITIES, - // A resolved managed worktree or folder workspace is never a floating terminal. WSL is left to - // the executing host's own create-support probe, which reads the resolved workspace rather - // than guessing from a client-side project runtime. + // The floating workspace has nowhere to keep a session, so it is decided here rather than left + // to the host probe below, which cannot answer for a workspace with no record. WSL still is: + // the create-support probe reads the resolved workspace rather than guessing from a + // client-side project runtime. + ...(placement.workspaceKind ? { workspaceKind: placement.workspaceKind } : {}), requiresTuiLaunchCommand: hasExplicitTuiLaunchCommand(settings, agent) }) if (!support.supported) { 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) +}) 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 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() +}) 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 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 diff --git a/src/main/claude-accounts/claude-account-service-login-process.test.ts b/src/main/claude-accounts/claude-account-service-login-process.test.ts index ec5572f5b3e..8aee047e5fa 100644 --- a/src/main/claude-accounts/claude-account-service-login-process.test.ts +++ b/src/main/claude-accounts/claude-account-service-login-process.test.ts @@ -40,6 +40,9 @@ vi.mock('./keychain', () => ({ writeManagedClaudeKeychainCredentials: vi.fn(async () => {}) })) +// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the doubles below implement every member the add-account login path reaches; this case drives the service only through addAccount. +const asServiceDouble = (double: unknown): T => double as T + describe('ClaudeAccountService credential capture', () => { beforeEach(() => { setPlatform('darwin') @@ -498,6 +501,79 @@ describe('ClaudeAccountService credential capture', () => { } }) + it('supersedes the login a closed Settings pane abandoned instead of queueing behind it', async () => { + setPlatform('linux') + vi.resetModules() + vi.mocked(readActiveClaudeKeychainCredentials).mockResolvedValue(null) + const children: (EventEmitter & { stdin: PassThrough; kill: ReturnType })[] = [] + // No pid: the POSIX teardown signals -pid as a process group, which must + // never reach a real group from a test double. + const spawnMock = vi.fn(() => { + const child = Object.assign(new EventEmitter(), { + stdin: new PassThrough(), + stdout: new PassThrough(), + stderr: new PassThrough(), + kill: vi.fn() + }) + children.push(child) + return child + }) + vi.doMock('node:child_process', () => ({ spawn: spawnMock })) + + try { + const { ClaudeAccountService } = await import('./service') + let settings = { + claudeManagedAccounts: [], + activeClaudeManagedAccountId: null, + activeClaudeManagedAccountIdsByRuntime: { host: null, wsl: {} } + } + const store = { + getSettings: vi.fn(() => settings), + updateSettings: vi.fn((updates: Partial) => { + settings = { ...settings, ...updates } + return settings + }) + } + const runtimeAuth = { + clearLastWrittenCredentialsJson: vi.fn(), + syncForCurrentSelection: vi.fn(async () => {}), + forceMaterializeCurrentSelectionForRollback: vi.fn(async () => {}) + } + const rateLimits = { + evictInactiveClaudeCache: vi.fn(), + refreshForClaudeAccountChange: vi.fn() + } + const service = new ClaudeAccountService( + asServiceDouble(store), + asServiceDouble(rateLimits), + asServiceDouble(runtimeAuth) + ) + + const abandoned = service.addAccount({ runtime: 'host' }) + const abandonedRejection = expect(abandoned).rejects.toThrow('Claude sign-in was cancelled.') + await vi.waitFor(() => { + expect(children.length).toBe(1) + }) + + // The user reopens Settings and clicks Add Account again. + const retry = service.addAccount({ runtime: 'host' }) + const retryRejection = expect(retry).rejects.toThrow() + + await abandonedRejection + expect(children[0].kill).toHaveBeenCalled() + // Why: the point of the fix — the second login starts now, not after the + // abandoned one's whole sign-in deadline elapses. + await vi.waitFor(() => { + expect(children.length).toBe(2) + }) + + children[1].emit('close', 1) + await retryRejection + } finally { + vi.doUnmock('node:child_process') + } + }) + it('uses taskkill to cancel the Windows Claude login process tree', async () => { setPlatform('win32') vi.resetModules() diff --git a/src/main/claude-accounts/service.ts b/src/main/claude-accounts/service.ts index 8f1077ba1a5..d1c79eaf17d 100644 --- a/src/main/claude-accounts/service.ts +++ b/src/main/claude-accounts/service.ts @@ -76,6 +76,7 @@ export class ClaudeAccountService { } async addAccount(target?: ClaudeAccountAddTarget): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.registration.add(target)) } @@ -87,14 +88,17 @@ export class ClaudeAccountService { } async reauthenticateAccount(accountId: string): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.registration.reauthenticate(accountId)) } async removeAccount(accountId: string): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.selection.remove(accountId)) } async selectAccount(accountId: string | null): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.selection.select(accountId)) } @@ -102,6 +106,7 @@ export class ClaudeAccountService { accountId: string | null, target?: ClaudeAccountSelectionTarget ): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.selection.select(accountId, target)) } @@ -109,6 +114,17 @@ export class ClaudeAccountService { return this.cancelPendingClaudeLogin?.() ?? false } + // Why before the queue, not inside it: the abandoned login owns the queue slot + // every later account action waits for. Called from the four the user drives, + // never from serializeMutation, which background work also uses. + private supersedePendingLogin(): void { + if (this.cancelPendingLogin()) { + console.info( + '[claude-accounts] Cancelled a pending Claude login superseded by a new request.' + ) + } + } + getRuntimeConfigDir(target?: ClaudeAccountSelectionTarget): string { return this.runtimeAuth.getRuntimeConfigDir(target) } 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/claude/claude-result-outcome.ts b/src/main/claude/claude-result-outcome.ts new file mode 100644 index 00000000000..5dbfbfffc71 --- /dev/null +++ b/src/main/claude/claude-result-outcome.ts @@ -0,0 +1,23 @@ +// What a Claude result frame says became of the turn it ends. +// +// Sole owner of the abort-reason list. Two readers depend on that distinction — +// the turn's durable outcome and whether the failure earns a visible row — and a +// second copy of the list would drift into disagreeing about whether a stop was +// the user's or the provider's. + +import type { AgentJournalTurnOutcome } from '../../shared/agent-session-journal-types' +import { claudeText } from './claude-structured-item-translation' + +/** The SDK reports the user's stop as an error result, so `is_error` alone cannot + * tell a cancellation from a fault; only these reasons do. */ +const CLAUDE_ABORTED_TERMINAL_REASONS = new Set(['aborted_streaming', 'aborted_tools']) + +/** A success-subtype result still carries `is_error` for an API error, so the flag + * is what decides, never the subtype. */ +export function claudeResultOutcome(message: Record): AgentJournalTurnOutcome { + if (message.is_error !== true) { + return 'success' + } + const reason = claudeText(message.terminal_reason) + return reason !== null && CLAUDE_ABORTED_TERMINAL_REASONS.has(reason) ? 'cancellation' : 'failure' +} diff --git a/src/main/claude/claude-structured-journal-translation-turn-timing.test.ts b/src/main/claude/claude-structured-journal-translation-turn-timing.test.ts index 12d9619ab3d..7bbb0b56b6a 100644 --- a/src/main/claude/claude-structured-journal-translation-turn-timing.test.ts +++ b/src/main/claude/claude-structured-journal-translation-turn-timing.test.ts @@ -116,6 +116,9 @@ describe('Claude structured turn timing', () => { expect(state.lifecycle().at(-1)).toEqual({ turnId: 'user-1', state: 'completed', + // A result with no error is the provider saying the turn worked, so the + // ordinary path carries a positive verdict rather than leaving it unknown. + outcome: 'success', startedAt: 1_000, completedAt: 4_500, userItemId: USER_1_KEY, diff --git a/src/main/claude/claude-structured-journal-translation.test.ts b/src/main/claude/claude-structured-journal-translation.test.ts index d9b8736c955..574547ee731 100644 --- a/src/main/claude/claude-structured-journal-translation.test.ts +++ b/src/main/claude/claude-structured-journal-translation.test.ts @@ -482,6 +482,12 @@ describe('Claude structured journal translation', () => { }) // The turn still settles: the error is an extra row, not a stuck lifecycle. expect(lifecycleAppends(state.items).at(-1)).toEqual(['turn-lifecycle:user-1', 'completed']) + // The arm stays `completed` on purpose — the host watched this turn finish — + // and `outcome` is the only thing that says it failed. Widening the arm + // instead would move every reader that switches on it. + expect(state.items.findLast((item) => item.identity.provider === 'legacy')?.body).toMatchObject( + { kind: 'turn', state: 'completed', outcome: 'failure' } + ) }) it('drops the stream state of turns that ended without their final frame', () => { diff --git a/src/main/claude/claude-structured-provider-fallback.ts b/src/main/claude/claude-structured-provider-fallback.ts index c1aa37e98b7..ebce2762c55 100644 --- a/src/main/claude/claude-structured-provider-fallback.ts +++ b/src/main/claude/claude-structured-provider-fallback.ts @@ -14,6 +14,7 @@ import { claudeText, type ClaudeMessageEnvelope } from './claude-structured-item-translation' +import { claudeResultOutcome } from './claude-result-outcome' export function claudeProviderFrameKind(message: Record): string { const type = claudeText(message.type) ?? 'unknown' @@ -45,11 +46,9 @@ export function isSettledClaudeResultKind(kind: string): boolean { export function claudeResultFailure( message: Record ): { text: string | null } | null { - if (message.is_error !== true) { - return null - } - const terminalReason = claudeText(message.terminal_reason) - if (terminalReason === 'aborted_streaming' || terminalReason === 'aborted_tools') { + // A cancellation is not a fault and earns no error row; the outcome classifier + // owns that distinction so this reader cannot drift from the turn's verdict. + if (claudeResultOutcome(message) !== 'failure') { return null } const result = claudeText(message.result)?.trim() diff --git a/src/main/claude/claude-turn-lifecycle-item.ts b/src/main/claude/claude-turn-lifecycle-item.ts index 7401cc0780c..2a31b49afdb 100644 --- a/src/main/claude/claude-turn-lifecycle-item.ts +++ b/src/main/claude/claude-turn-lifecycle-item.ts @@ -1,11 +1,12 @@ import type { AgentJournalItemIdentity, - AgentJournalTurnItem + AgentJournalTurnItem, + AgentJournalTurnOutcome } from '../../shared/agent-session-journal-types' import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' import { agentJournalTurnBody } from '../../shared/agent-session-turn-record' import type { StructuredAgentSessionAppendOptions } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' -import { claudeText } from './claude-structured-item-translation' +import { claudeResultOutcome } from './claude-result-outcome' export type ClaudeCurrentTurn = { sessionId: string @@ -22,21 +23,27 @@ export type ClaudeCurrentTurn = { export type ClaudeTurnEnd = { state: 'completed' | 'interrupted' completedAt: number + /** Only an end the PROVIDER reported carries one. An end the host inferred — + * the child going away, a new turn superseding this one — leaves it absent, + * which reads as unknown rather than claiming the turn worked. */ + outcome?: AgentJournalTurnOutcome /** The SDK's own measured turn duration; only a result frame carries one. */ durationMs?: number } -/** A result the SDK reports as aborted is the user's stop, not the model's end. */ +/** A result the SDK reports as aborted is the user's stop, not the model's end. + * The lifecycle state is deliberately unchanged by the outcome: a failed turn + * is still a turn the host watched finish, and only `outcome` says it failed. */ export function claudeTurnEndForResult( message: Record, completedAt: number ): ClaudeTurnEnd { - const reason = message.is_error === true ? claudeText(message.terminal_reason) : null + const outcome = claudeResultOutcome(message) const durationMs = message.duration_ms return { - state: - reason === 'aborted_streaming' || reason === 'aborted_tools' ? 'interrupted' : 'completed', + state: outcome === 'cancellation' ? 'interrupted' : 'completed', completedAt, + outcome, ...(typeof durationMs === 'number' && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {}) @@ -83,6 +90,7 @@ export function claudeTurnLifecycleItem( ? { turnId, state: end.state, + ...(end.outcome === undefined ? {} : { outcome: end.outcome }), startedAt, ...requested, completedAt: end.completedAt, diff --git a/src/main/claude/claude-turn-outcome.test.ts b/src/main/claude/claude-turn-outcome.test.ts new file mode 100644 index 00000000000..8223652458b --- /dev/null +++ b/src/main/claude/claude-turn-outcome.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it, vi } from 'vitest' +import type { + AgentJournalItemBody, + AgentJournalItemIdentity +} from '../../shared/agent-session-journal-types' +import { + readAgentJournalTurn, + readAgentJournalTurnOutcome +} from '../../shared/agent-session-turn-record' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { claudeResultOutcome } from './claude-result-outcome' +import { createClaudeJournalTranslator } from './claude-structured-journal-translation' +import { claudeResultFailure } from './claude-structured-provider-fallback' +import { claudeTurnEndForResult, claudeTurnLifecycleItem } from './claude-turn-lifecycle-item' + +function sinkState() { + const items: { identity: AgentJournalItemIdentity; body: AgentJournalItemBody }[] = [] + const sink: StructuredAgentSessionEventSink = { + appendItem: (identity, body) => items.push({ identity, body }), + appendTombstone: () => {}, + publish: vi.fn() + } + return { sink, items } +} + +function userTurn(uuid: string) { + return { + type: 'message' as const, + sessionId: 'orca-session', + startsTurn: true as const, + message: { + type: 'user', + uuid, + session_id: 'claude-session', + parent_tool_use_id: null, + message: { role: 'user', content: [{ type: 'text', text: 'summarize this' }] } + } + } +} + +function result(fields: Record) { + return { type: 'result', duration_ms: 1_200, ...fields } +} + +const TURN = { + sessionId: 'orca-session', + turnId: 'user-1', + startedAt: 1_000, + userItemId: 'claude:user-1' +} + +describe('claudeResultOutcome', () => { + it('separates the provider failing from the user stopping it', () => { + expect(claudeResultOutcome(result({ subtype: 'success', is_error: false }))).toBe('success') + // The SDK reports a user stop as an error result, so the flag alone cannot + // tell a cancellation from a fault; only the terminal reason does. + expect( + claudeResultOutcome(result({ is_error: true, terminal_reason: 'aborted_streaming' })) + ).toBe('cancellation') + expect(claudeResultOutcome(result({ is_error: true, terminal_reason: 'aborted_tools' }))).toBe( + 'cancellation' + ) + expect(claudeResultOutcome(result({ is_error: true, terminal_reason: 'api_error' }))).toBe( + 'failure' + ) + // A reason this build has never seen is still a reported failure, not a stop. + expect(claudeResultOutcome(result({ is_error: true, terminal_reason: 'future_reason' }))).toBe( + 'failure' + ) + expect(claudeResultOutcome(result({ is_error: true }))).toBe('failure') + }) + + it('reads the success-subtype result that carries an API error as a failure', () => { + // The real captured shape: subtype `success`, `is_error` true. Trusting the + // subtype is what let an API error be recorded as a clean turn. + expect( + claudeResultOutcome( + result({ + subtype: 'success', + is_error: true, + result: 'API Error: 529 upstream overloaded', + terminal_reason: 'api_error' + }) + ) + ).toBe('failure') + }) + + it('is the single owner of the abort list the visible error row also reads', () => { + // One classifier, so the durable verdict and the error row cannot disagree + // about whether a stop was the user's. + for (const reason of ['aborted_streaming', 'aborted_tools', 'api_error', 'future_reason']) { + const message = result({ is_error: true, terminal_reason: reason, result: 'text' }) + expect(claudeResultFailure(message) !== null).toBe(claudeResultOutcome(message) === 'failure') + } + expect(claudeResultFailure(result({ is_error: false }))).toBeNull() + }) +}) + +describe('claudeTurnEndForResult', () => { + it('records the verdict without moving the lifecycle arm', () => { + // The arms are what six readers switch on. A failed turn is still a turn the + // host watched finish, so it stays `completed` and only `outcome` says more. + expect( + claudeTurnEndForResult(result({ is_error: true, terminal_reason: 'api_error' }), 9_000) + ).toEqual({ state: 'completed', outcome: 'failure', completedAt: 9_000, durationMs: 1_200 }) + expect(claudeTurnEndForResult(result({ is_error: false }), 9_000)).toEqual({ + state: 'completed', + outcome: 'success', + completedAt: 9_000, + durationMs: 1_200 + }) + expect( + claudeTurnEndForResult(result({ is_error: true, terminal_reason: 'aborted_tools' }), 9_000) + ).toEqual({ + state: 'interrupted', + outcome: 'cancellation', + completedAt: 9_000, + durationMs: 1_200 + }) + }) +}) + +describe('claudeTurnLifecycleItem', () => { + it('carries the verdict onto the terminal row and never onto the running one', () => { + expect(readAgentJournalTurn(claudeTurnLifecycleItem(TURN).body)).toEqual({ + turnId: 'user-1', + state: 'running', + startedAt: 1_000, + userItemId: 'claude:user-1' + }) + expect( + readAgentJournalTurn( + claudeTurnLifecycleItem(TURN, { + state: 'completed', + outcome: 'failure', + completedAt: 4_000 + }).body + ) + ).toEqual({ + turnId: 'user-1', + state: 'completed', + outcome: 'failure', + startedAt: 1_000, + completedAt: 4_000, + userItemId: 'claude:user-1' + }) + }) + + it('omits the verdict for an end the host inferred rather than heard', () => { + // No provider result arrived, so nothing named a verdict. Writing one here + // would be the host guessing, and absent already means unknown. + const body = claudeTurnLifecycleItem(TURN, { state: 'interrupted', completedAt: 4_000 }).body + expect(body).not.toHaveProperty('outcome') + expect(readAgentJournalTurn(body)).toEqual({ + turnId: 'user-1', + state: 'interrupted', + startedAt: 1_000, + completedAt: 4_000, + userItemId: 'claude:user-1' + }) + }) +}) + +describe('a turn end the host inferred', () => { + it.each([ + [ + 'the child ending', + (translator: ReturnType) => + translator.handle({ type: 'ended', sessionId: 'orca-session', reason: 'closed' }) + ], + [ + 'a new turn superseding it', + (translator: ReturnType) => + translator.handle(userTurn('user-2')) + ] + ])('records no outcome for a turn ended by %s', (_label, end) => { + const state = sinkState() + const translator = createClaudeJournalTranslator({ sink: state.sink }) + + translator.handle(userTurn('user-1')) + end(translator) + + // No result frame arrived, so the provider never said what became of the + // turn. The host observed the END — the arm stays `interrupted` — but the + // verdict is unknown, and guessing `success` here is what a notification + // gated on the outcome would fire on. + const settled = state.items.find( + (item) => + item.identity.provider === 'legacy' && + item.identity.recordId === 'turn-lifecycle:user-1' && + readAgentJournalTurn(item.body)?.state === 'interrupted' + ) + expect(readAgentJournalTurn(settled?.body)).toMatchObject({ state: 'interrupted' }) + expect(settled?.body).not.toHaveProperty('outcome') + expect(readAgentJournalTurnOutcome(readAgentJournalTurn(settled?.body))).toBeNull() + }) +}) diff --git a/src/main/codex-accounts/codex-login-auth-snapshot.ts b/src/main/codex-accounts/codex-login-auth-snapshot.ts new file mode 100644 index 00000000000..5347281f2e3 --- /dev/null +++ b/src/main/codex-accounts/codex-login-auth-snapshot.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs' + +/** The bytes of a managed home's auth.json, null when absent, undefined when unreadable. */ +export function readLoginAuthSnapshot(authJsonPath: string): string | null | undefined { + try { + return readFileSync(authJsonPath, 'utf-8') + } catch (error) { + const code = (error as NodeJS.ErrnoException).code + if (code === 'ENOENT' || code === 'ENOTDIR') { + return null + } + // Why: codex can atomically replace auth.json while the poll runs; a later + // poll will observe the stable credential. An unreadable initial file must + // disable the shortcut rather than look like a fresh login. + return undefined + } +} + +/** Whether a login has written credentials of its own since the snapshot was taken. */ +export function loginAuthChanged( + initial: string | null | undefined, + current: string | null | undefined +): boolean { + // Why: metadata-only touches can happen before OAuth finishes. Requiring new + // credential bytes prevents reauthentication from being killed prematurely. + return initial !== undefined && current !== undefined && current !== null && current !== initial +} diff --git a/src/main/codex-accounts/codex-login-auth-url.test.ts b/src/main/codex-accounts/codex-login-auth-url.test.ts new file mode 100644 index 00000000000..9dddaef048c --- /dev/null +++ b/src/main/codex-accounts/codex-login-auth-url.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest' +import { parseCodexLoginAuthUrl } from './codex-login-auth-url' + +const AUTH_URL = 'https://auth.openai.com/oauth/authorize?client_id=orca&state=abc123' + +describe('parseCodexLoginAuthUrl', () => { + it('reads the link codex prints under its browser notice', () => { + expect( + parseCodexLoginAuthUrl( + `Starting local login server on http://localhost:1455.\nIf your browser did not open, navigate to this URL to authenticate:\n\n${AUTH_URL}\n` + ) + ).toBe(AUTH_URL) + }) + + it('ignores the local server line, which authenticates nothing', () => { + expect(parseCodexLoginAuthUrl('Starting local login server on http://localhost:1455.\n')).toBe( + null + ) + }) + + it('waits for a chunk boundary rather than publishing a truncated link', () => { + const truncated = `If your browser did not open, navigate to this URL to authenticate:\n\n${AUTH_URL.slice(0, 40)}` + expect(parseCodexLoginAuthUrl(truncated)).toBe(null) + expect(parseCodexLoginAuthUrl(`${truncated}${AUTH_URL.slice(40)}\n`)).toBe(AUTH_URL) + }) + + it('survives the escape sequences a coloured CLI writes around the link', () => { + expect( + parseCodexLoginAuthUrl( + `navigate to this URL to authenticate:\n\n${AUTH_URL}\n` + ) + ).toBe(AUTH_URL) + }) + + it('drops sentence punctuation that follows the link', () => { + expect( + parseCodexLoginAuthUrl(`navigate to this URL to authenticate: ${AUTH_URL}. Then return here.`) + ).toBe(AUTH_URL) + }) + + it('reports nothing for output without a link yet', () => { + expect(parseCodexLoginAuthUrl('')).toBe(null) + expect(parseCodexLoginAuthUrl('Codex login failed: network unreachable\n')).toBe(null) + }) + + it('offers no link at all rather than an unrelated one when the notice is missing', () => { + expect( + parseCodexLoginAuthUrl('A new version of codex is available: https://openai.com/codex\n') + ).toBe(null) + }) +}) diff --git a/src/main/codex-accounts/codex-login-auth-url.ts b/src/main/codex-accounts/codex-login-auth-url.ts new file mode 100644 index 00000000000..024f0b6e73e --- /dev/null +++ b/src/main/codex-accounts/codex-login-auth-url.ts @@ -0,0 +1,32 @@ +import { stripAnsiEscapeSequences } from '../../shared/ansi-escape-sequences' + +const AUTH_URL_MARKER = 'navigate to this url to authenticate:' + +/** + * The browser sign-in link `codex login` prints, or null while its output has + * not carried a complete one yet. + */ +export function parseCodexLoginAuthUrl(output: string): string | null { + const plain = stripAnsiEscapeSequences(output) + const markerIndex = plain.toLowerCase().indexOf(AUTH_URL_MARKER) + // Why the marker is required: without it the first https link codex happens to + // print — an update notice, a docs link — would be offered as the sign-in link. + // If codex rewords the line, no link beats the wrong one. + if (markerIndex === -1) { + return null + } + const searchable = plain.slice(markerIndex + AUTH_URL_MARKER.length) + // Why: the trailing whitespace is required, not incidental. Output arrives in + // chunks, and a flush that ends mid-token would otherwise publish a truncated + // link that authenticates nothing. + const match = /(https:\/\/\S+?)[.,;:)\]]*\s/.exec(searchable) + if (!match) { + return null + } + try { + // Only the shape is trusted here; the pattern already fixed the scheme. + return new URL(match[1]).toString() + } catch { + return null + } +} diff --git a/src/main/codex-accounts/codex-login-session.ts b/src/main/codex-accounts/codex-login-session.ts index 6df83c05e18..3595351ef37 100644 --- a/src/main/codex-accounts/codex-login-session.ts +++ b/src/main/codex-accounts/codex-login-session.ts @@ -1,19 +1,24 @@ -import { readFileSync } from 'node:fs' import { join } from 'node:path' import type { WindowsHostInteractiveLoginSpawn } from '../../shared/windows-interactive-login-spawn' import { buildWindowsHostInteractiveLoginSpawn } from '../../shared/windows-interactive-login-spawn' import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution' +import { CODEX_LOGIN_CANCELLED_MESSAGE } from '../../shared/codex-auth-errors' import { parseWslUncPath } from '../../shared/wsl-paths' import { resolveCodexCommand } from '../codex-cli/command' import { getSpawnArgsForWindows } from '../win32-utils' import { runWslProcess } from '../wsl/wsl-runner' +import { parseCodexLoginAuthUrl } from './codex-login-auth-url' +import { loginAuthChanged, readLoginAuthSnapshot } from './codex-login-auth-snapshot' import { buildWslCodexAvailabilityScript, buildWslCodexLoginArgs, WSL_CODEX_AVAILABILITY_TIMEOUT_MS } from './wsl-codex-command' -const LOGIN_TIMEOUT_MS = 120_000 +// Why: matches Claude's window. Signing in through a copied link — a second +// browser, a password manager, an incognito window — routinely outlasts two +// minutes, and the old 120s deadline failed those users mid-flow. +const LOGIN_TIMEOUT_MS = 180_000 const MAX_LOGIN_OUTPUT_CHARS = 4_000 const WINDOWS_LOGIN_AUTH_POLL_INTERVAL_MS = 500 const WINDOWS_LOGIN_POST_AUTH_EXIT_GRACE_MS = 5_000 @@ -50,35 +55,47 @@ type CodexLoginSessionDependencies = { child: CodexLoginChild, interactiveLogin?: WindowsHostInteractiveLoginSpawn | null ) => void + /** Registers the handle that abandons this login; the caller clears it. */ + setCancel: (cancel: () => boolean) => void + /** The browser link codex printed, published as soon as it is complete. */ + onAuthUrl: (url: string) => void } -function readLoginAuthSnapshot(authJsonPath: string): string | null | undefined { - try { - return readFileSync(authJsonPath, 'utf-8') - } catch (error) { - const code = (error as NodeJS.ErrnoException).code - if (code === 'ENOENT' || code === 'ENOTDIR') { - return null - } - // Why: codex can atomically replace auth.json while the poll runs; a later - // poll will observe the stable credential. An unreadable initial file must - // disable the shortcut rather than look like a fresh login. - return undefined - } -} - -function loginAuthChanged( - initial: string | null | undefined, - current: string | null | undefined -): boolean { - // Why: metadata-only touches can happen before OAuth finishes. Requiring new - // credential bytes prevents reauthentication from being killed prematurely. - return initial !== undefined && current !== undefined && current !== null && current !== initial +type LoginCancellation = { + isCancelled: () => boolean + setSpawnedCancel: (cancel: () => boolean) => void } export async function runCodexLoginSession( managedHomePath: string, dependencies: CodexLoginSessionDependencies +): Promise { + let cancelSpawnedLogin: (() => boolean) | null = null + let cancelled = false + dependencies.setCancel(() => { + // Why: only an accepted cancel latches. A spawned login that refuses — + // because it already authenticated — must stay cancellable, or the Cancel + // button and the next add both go dead for the rest of the deadline. + if (cancelled || cancelSpawnedLogin?.() === false) { + return false + } + // A cancel before the spawn has no tree to kill; the pre-spawn probe reads + // this flag instead of opening a browser nobody is waiting for. + cancelled = true + return true + }) + await runCodexLoginProcess(managedHomePath, dependencies, { + isCancelled: () => cancelled, + setSpawnedCancel: (cancel) => { + cancelSpawnedLogin = cancel + } + }) +} + +async function runCodexLoginProcess( + managedHomePath: string, + dependencies: CodexLoginSessionDependencies, + cancellation: LoginCancellation ): Promise { const wslInfo = parseWslUncPath(managedHomePath) if (wslInfo) { @@ -87,9 +104,15 @@ export async function runCodexLoginSession( // Why: reauthentication starts with an existing auth.json. Only new auth // bytes prove this login completed; existence alone would kill the // Windows OAuth flow five seconds after it opened. + // WSL keeps its baseline unread — the UNC round trip belongs nowhere in the + // pre-spawn path — so there is nothing to compare a WSL home against. const initialAuthSnapshot = wslInfo ? null : readLoginAuthSnapshot(join(managedHomePath, 'auth.json')) + const hasAuthBaseline = !wslInfo + if (cancellation.isCancelled()) { + throw new Error(CODEX_LOGIN_CANCELLED_MESSAGE) + } await new Promise((resolvePromise, rejectPromise) => { const spawnConfig = wslInfo @@ -119,6 +142,22 @@ export async function runCodexLoginSession( } } + // Why its own buffer: a stderr chunk interleaved between two halves of the + // link would end the match early, and the published link never changes. + let stdoutText = '' + let publishedAuthUrl = false + const publishAuthUrl = (chunk: Buffer): void => { + if (publishedAuthUrl) { + return + } + stdoutText = `${stdoutText}${chunk.toString()}`.slice(-MAX_LOGIN_OUTPUT_CHARS) + const authUrl = parseCodexLoginAuthUrl(stdoutText) + if (authUrl) { + publishedAuthUrl = true + dependencies.onAuthUrl(authUrl) + } + } + let timeout: ReturnType | null = null let authWatchInterval: ReturnType | null = null let postAuthExitTimeout: ReturnType | null = null @@ -138,6 +177,7 @@ export async function runCodexLoginSession( postAuthExitTimeout = null } child.stdout?.off('data', appendOutput) + child.stdout?.off('data', publishAuthUrl) child.stderr?.off('data', appendOutput) child.off('error', onError) child.off('close', onClose) @@ -153,6 +193,23 @@ export async function runCodexLoginSession( callback() } + cancellation.setSpawnedCancel(() => { + // Why: once codex has written new credential bytes the sign-in already + // succeeded, and rejecting here would send the caller's rollback at the + // home it just authenticated. Nothing left to cancel — let it settle. + // Without a baseline (WSL) an existing auth.json says nothing about this + // login, so refusing on it would make a WSL reauth uncancellable. + const alreadyAuthenticated = + hasAuthBaseline && + loginAuthChanged(initialAuthSnapshot, readLoginAuthSnapshot(authJsonPath)) + if (settled || alreadyAuthenticated) { + return false + } + dependencies.killProcessTree(child, spawnConfig.interactiveLogin) + settle(() => rejectPromise(new Error(CODEX_LOGIN_CANCELLED_MESSAGE))) + return true + }) + const timeoutError = new Error('Codex sign-in took too long to finish. Please try again.') timeout = setTimeout(() => { dependencies.killProcessTree(child, spawnConfig.interactiveLogin) @@ -221,6 +278,7 @@ export async function runCodexLoginSession( } child.stdout?.on('data', appendOutput) + child.stdout?.on('data', publishAuthUrl) child.stderr?.on('data', appendOutput) child.on('error', onError) child.on('close', onClose) diff --git a/src/main/codex-accounts/service-abandoned-login-supersede.test.ts b/src/main/codex-accounts/service-abandoned-login-supersede.test.ts new file mode 100644 index 00000000000..11743258fe5 --- /dev/null +++ b/src/main/codex-accounts/service-abandoned-login-supersede.test.ts @@ -0,0 +1,192 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { EventEmitter } from 'node:events' +import { existsSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { PassThrough } from 'node:stream' +import { + createCodexAuthJson, + createRateLimits, + createRuntimeHome, + createSettings, + createStore, + registerCodexAccountsTestHomes, + testState +} from './service-test-harness' + +vi.mock('electron', () => ({ + app: { + getPath: () => testState.userDataDir + } +})) + +vi.mock('node:os', async () => { + const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + return { + ...actual, + homedir: () => testState.fakeHomeDir + } +}) + +// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the harness doubles implement every member the add-account login path reaches; this test drives the service only through addAccount, cancelPendingLogin and the login URL subscription. +const asServiceDouble = (double: unknown): T => double as T + +type StubLoginChild = EventEmitter & { + stdout: PassThrough + stderr: PassThrough + kill: () => boolean + exitCode: number | null + signalCode: string | null +} + +function createStubLoginChild(): StubLoginChild { + const child: StubLoginChild = Object.assign(new EventEmitter(), { + stdout: new PassThrough(), + stderr: new PassThrough(), + exitCode: null, + signalCode: null, + kill: () => true + }) + // The real codex CLI forwards SIGTERM to its native child and exits. + child.kill = vi.fn(() => { + child.exitCode = 143 + return true + }) + return child +} + +/** A service whose `codex login` never finishes on its own. */ +async function createServiceWithHangingLogin(): Promise<{ + service: { + addAccount: () => Promise<{ accounts: { email: string }[] }> + selectAccount: (accountId: string | null) => Promise + cancelPendingLogin: () => boolean + getPendingLoginUrl: () => string | null + onPendingLoginUrlChanged: (listener: (url: string | null) => void) => void + } + children: StubLoginChild[] + /** The `CODEX_HOME` each login was spawned against. */ + loginHomes: string[] +}> { + vi.resetModules() + const children: StubLoginChild[] = [] + const loginHomes: string[] = [] + vi.doMock('node:child_process', () => ({ + execFileSync: vi.fn(), + spawn: vi.fn((_command: string, _args: string[], options: { env: NodeJS.ProcessEnv }) => { + loginHomes.push(options.env.CODEX_HOME ?? '') + const child = createStubLoginChild() + children.push(child) + return child + }) + })) + vi.doMock('../codex-cli/command', () => ({ + resolveCodexCommand: () => 'codex' + })) + const { CodexAccountService } = await import('./service') + const service = new CodexAccountService( + asServiceDouble(createStore(createSettings())), + asServiceDouble(createRateLimits()), + asServiceDouble(createRuntimeHome()) + ) + return { service, children, loginHomes } +} + +describe('CodexAccountService abandoned login', () => { + registerCodexAccountsTestHomes() + + afterEach(() => { + vi.doUnmock('node:child_process') + vi.doUnmock('../codex-cli/command') + }) + + it('supersedes the login a closed Settings pane abandoned instead of queueing behind it', async () => { + const { service, children } = await createServiceWithHangingLogin() + const abandoned = service.addAccount() + const abandonedRejection = expect(abandoned).rejects.toThrow('Codex sign-in was cancelled.') + await vi.waitUntil(() => children.length === 1) + + // The user reopens Settings and clicks Add Account again. + const retry = service.addAccount() + const retryRejection = expect(retry).rejects.toThrow() + + await abandonedRejection + expect(children[0].kill).toHaveBeenCalledTimes(1) + // Why: the point of the fix — the second login starts now, not after the + // abandoned one's whole sign-in deadline elapses. + await vi.waitUntil(() => children.length === 2) + expect(children[1].kill).not.toHaveBeenCalled() + + service.cancelPendingLogin() + await retryRejection + }) + + it('frees the queue for a plain account switch too, not only for another add', async () => { + const { service, children } = await createServiceWithHangingLogin() + const abandoned = service.addAccount() + const abandonedRejection = expect(abandoned).rejects.toThrow('Codex sign-in was cancelled.') + await vi.waitUntil(() => children.length === 1) + + // Why: switching to the system default is the commonest thing a user does + // after giving up on a sign-in, and it shares the add's mutation queue. + await service.selectAccount(null) + await abandonedRejection + expect(children[0].kill).toHaveBeenCalled() + }) + + it('refuses to cancel a sign-in that already wrote credentials, and keeps the account', async () => { + const { service, children, loginHomes } = await createServiceWithHangingLogin() + const pending = service.addAccount() + await vi.waitUntil(() => children.length === 1) + + // The browser half of the OAuth flow finishes while the CLI lingers. + writeFileSync( + join(loginHomes[0], 'auth.json'), + createCodexAuthJson('user@example.com', 'provider-account-1', 'refresh-token'), + 'utf-8' + ) + + // Why: cancelling here would send the rollback at a home that just + // authenticated. There is nothing left to cancel. + expect(service.cancelPendingLogin()).toBe(false) + expect(children[0].kill).not.toHaveBeenCalled() + + children[0].emit('close', 0) + const accounts = await pending + expect(accounts.accounts.map((account) => account.email)).toEqual(['user@example.com']) + expect(existsSync(join(loginHomes[0], 'auth.json'))).toBe(true) + }) + + it('reports whether a pending login was there to cancel', async () => { + const { service, children } = await createServiceWithHangingLogin() + const pending = service.addAccount() + const rejection = expect(pending).rejects.toThrow('Codex sign-in was cancelled.') + await vi.waitUntil(() => children.length === 1) + + expect(service.cancelPendingLogin()).toBe(true) + await rejection + expect(service.cancelPendingLogin()).toBe(false) + }) + + it('publishes the sign-in link codex prints and drops it when the login ends', async () => { + const { service, children } = await createServiceWithHangingLogin() + const published: (string | null)[] = [] + service.onPendingLoginUrlChanged((url) => published.push(url)) + + const pending = service.addAccount() + const rejection = expect(pending).rejects.toThrow('Codex sign-in was cancelled.') + await vi.waitUntil(() => children.length === 1) + + const authUrl = 'https://auth.openai.com/oauth/authorize?client_id=orca&state=abc' + children[0].stdout.write( + `Starting local login server on http://localhost:1455.\nIf your browser did not open, navigate to this URL to authenticate:\n\n${authUrl}\n` + ) + await vi.waitUntil(() => service.getPendingLoginUrl() === authUrl) + expect(published).toEqual([authUrl]) + + service.cancelPendingLogin() + await rejection + // Why: the link dies with the login server it points back at. + expect(service.getPendingLoginUrl()).toBeNull() + expect(published).toEqual([authUrl, null]) + }) +}) diff --git a/src/main/codex-accounts/service-login-process-teardown.test.ts b/src/main/codex-accounts/service-login-process-teardown.test.ts index a49877a196d..42753d4d59b 100644 --- a/src/main/codex-accounts/service-login-process-teardown.test.ts +++ b/src/main/codex-accounts/service-login-process-teardown.test.ts @@ -70,7 +70,7 @@ describe('CodexAccountService config sync', () => { 'Codex sign-in took too long to finish.' ) - await vi.advanceTimersByTimeAsync(120_000) + await vi.advanceTimersByTimeAsync(180_000) await rejection expect(child.kill).toHaveBeenCalledTimes(1) @@ -89,7 +89,10 @@ describe('CodexAccountService config sync', () => { vi.resetModules() vi.useFakeTimers() const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')! - Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }) + Object.defineProperty(process, 'platform', { + value: 'win32', + configurable: true + }) const child = new EventEmitter() as EventEmitter & { stdout: PassThrough stderr: PassThrough @@ -162,7 +165,10 @@ describe('CodexAccountService config sync', () => { vi.resetModules() vi.useFakeTimers() const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')! - Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }) + Object.defineProperty(process, 'platform', { + value: 'win32', + configurable: true + }) const child = new EventEmitter() as EventEmitter & { stdout: PassThrough stderr: PassThrough @@ -182,7 +188,9 @@ describe('CodexAccountService config sync', () => { execFileSync: execFileSyncMock, spawn: vi.fn(() => child) })) - vi.doMock('../codex-cli/command', () => ({ resolveCodexCommand: () => 'codex' })) + vi.doMock('../codex-cli/command', () => ({ + resolveCodexCommand: () => 'codex' + })) const authPath = join(testState.fakeHomeDir, 'auth.json') writeFileSync( authPath, @@ -198,7 +206,9 @@ describe('CodexAccountService config sync', () => { createRuntimeHome() as never ) const loginPromise = ( - service as unknown as { runCodexLogin(managedHomePath: string): Promise } + service as unknown as { + runCodexLogin(managedHomePath: string): Promise + } ).runCodexLogin(testState.fakeHomeDir) await vi.advanceTimersByTimeAsync(6_000) diff --git a/src/main/codex-accounts/service.ts b/src/main/codex-accounts/service.ts index 6f5a4a9f0b7..67f8ebb218a 100644 --- a/src/main/codex-accounts/service.ts +++ b/src/main/codex-accounts/service.ts @@ -80,6 +80,9 @@ function killLoginProcessTree( export class CodexAccountService { // Why: serialize the read-modify-write of settings; overlapping calls (e.g. double-click Add) would lose updates. private mutationQueue: Promise = Promise.resolve() + private cancelPendingCodexLogin: (() => boolean) | null = null + private pendingLoginUrl: string | null = null + private readonly pendingLoginUrlListeners = new Set<(url: string | null) => void>() private readonly identity: CodexAccountIdentity private readonly configMirror: CodexConfigMirror private readonly managedHomePaths: CodexManagedHomePath @@ -161,9 +164,46 @@ export class CodexAccountService { } async addAccount(target?: CodexAccountAddTarget): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.registration.add(target)) } + /** Abandons the login waiting on a browser, if any. True when one was stopped. */ + cancelPendingLogin(): boolean { + return this.cancelPendingCodexLogin?.() ?? false + } + + /** The sign-in link of the login waiting on a browser, for a late-joining renderer. */ + getPendingLoginUrl(): string | null { + return this.pendingLoginUrl + } + + /** Registration lasts the process's lifetime; there is no teardown to hand back. */ + onPendingLoginUrlChanged(listener: (url: string | null) => void): void { + this.pendingLoginUrlListeners.add(listener) + } + + private setPendingLoginUrl(url: string | null): void { + // Why the guard: every login that ends before printing a link clears an + // already-empty value, and each change reaches every window. + if (this.pendingLoginUrl === url) { + return + } + this.pendingLoginUrl = url + for (const listener of this.pendingLoginUrlListeners) { + listener(url) + } + } + + // Why before the queue, not inside it: the abandoned login owns the queue slot + // every later account action waits for. Called from the four the user drives, + // never from serializeMutation, which background reset-credit work also uses. + private supersedePendingLogin(): void { + if (this.cancelPendingLogin()) { + console.info('[codex-accounts] Cancelled a pending Codex login superseded by a new request.') + } + } + /** * Registers a managed Codex account from an already-authenticated `CODEX_HOME` * instead of driving `codex login` here. Lets the `orca account add --agent codex` @@ -181,14 +221,17 @@ export class CodexAccountService { accountId: string, options?: CodexAccountReauthenticateOptions ): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.registration.reauthenticate(accountId, options)) } async removeAccount(accountId: string): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.selection.remove(accountId)) } async selectAccount(accountId: string | null): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.selection.select(accountId)) } @@ -196,6 +239,7 @@ export class CodexAccountService { accountId: string | null, target?: CodexAccountSelectionTarget ): Promise { + this.supersedePendingLogin() return this.serializeMutation(() => this.selection.select(accountId, target)) } @@ -234,16 +278,27 @@ export class CodexAccountService { } private async runCodexLogin(managedHomePath: string): Promise { - await runCodexLoginSession(managedHomePath, { - wslCommand: 'wsl.exe', - spawn: ({ command, args, env, stdio }) => - spawn(command, args, { - stdio, - // Why: hide the outer wrapper only. A dedicated login console stays visible. - windowsHide: true, - env - }), - killProcessTree: killLoginProcessTree - }) + try { + await runCodexLoginSession(managedHomePath, { + wslCommand: 'wsl.exe', + spawn: ({ command, args, env, stdio }) => + spawn(command, args, { + stdio, + // Why: hide the outer wrapper only. A dedicated login console stays visible. + windowsHide: true, + env + }), + killProcessTree: killLoginProcessTree, + setCancel: (cancel) => { + this.cancelPendingCodexLogin = cancel + }, + onAuthUrl: (url) => this.setPendingLoginUrl(url) + }) + } finally { + // Why: both die with the login server — no surface may keep offering a + // link nothing is listening on, or a cancel with nothing to cancel. + this.cancelPendingCodexLogin = null + this.setPendingLoginUrl(null) + } } } 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) ) ) diff --git a/src/main/codex/codex-structured-journal-translation-restore.ts b/src/main/codex/codex-structured-journal-translation-restore.ts index 5631b8a132c..7005c10a43b 100644 --- a/src/main/codex/codex-structured-journal-translation-restore.ts +++ b/src/main/codex/codex-structured-journal-translation-restore.ts @@ -7,6 +7,7 @@ import { import type { CodexJournalTranslationAdmission } from './codex-structured-journal-translation' import { codexTurnLifecycleState, + codexTurnOutcome, codexTurnUserItemId } from './codex-structured-journal-translation-turns' import { readCodexTurnDurationMs, readCodexTurnStatus } from './codex-structured-thread-facts' @@ -103,9 +104,12 @@ function historicalTurnLifecycle( return null } const durationMs = readCodexTurnDurationMs(turn) + const status = readCodexTurnStatus(turn) + const outcome = codexTurnOutcome(status) return { turnId, - state: codexTurnLifecycleState(readCodexTurnStatus(turn)), + state: codexTurnLifecycleState(status), + ...(outcome ? { outcome } : {}), userItemId: codexTurnUserItemId(threadId, turnId), startedAt: startedAt * 1000, completedAt: completedAt * 1000, diff --git a/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts b/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts index 40adce6ed68..b1aee0a66f6 100644 --- a/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts +++ b/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts @@ -1,4 +1,7 @@ -import type { AgentJournalTurnLifecycle } from '../../shared/agent-session-journal-types' +import type { + AgentJournalTurnLifecycle, + AgentJournalTurnOutcome +} from '../../shared/agent-session-journal-types' import { agentJournalSubmissionKey } from '../../shared/agent-session-journal-item-key' import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' import { @@ -14,6 +17,7 @@ import { import type { CodexDispatchRequestOrigin } from './codex-structured-dispatch-echo' import { codexTurnLifecycleState, + codexTurnOutcome, codexTurnUserItemId, publishCodexTurnLifecycle } from './codex-structured-journal-translation-turns' @@ -139,15 +143,15 @@ export class CodexJournalTurnBoundaries { // the turn that spawned them and go on reporting into the same group, so a // turn boundary is no evidence contact was lost. Only `settleSession` may // write `unverifiable`. + const status = readCodexTurnStatus(event.params) const turnLifecycle = event.threadId === this.deps.primaryThreadId() - ? this.settled( - event.threadId, - turnId, - codexTurnLifecycleState(readCodexTurnStatus(event.params)), - this.receiptTime(event), - readCodexTurnDurationMs(event.params) - ) + ? this.settled(event.threadId, turnId, { + state: codexTurnLifecycleState(status), + outcome: codexTurnOutcome(status), + completedAt: this.receiptTime(event), + durationMs: readCodexTurnDurationMs(event.params) + }) : null const requestOrigin = this.deps.activeTurns.requestOrigin(event.threadId, turnId) const latestDispatchSequence = this.deps.activeTurns.latestDispatchSequence( @@ -181,25 +185,31 @@ export class CodexJournalTurnBoundaries { return admission } - /** Terminal lifecycle for a remembered turn; `startedAt` is absent when the start was never seen. */ + /** Terminal lifecycle for a remembered turn; `startedAt` is absent when the start was never seen. + * The verdict travels as one record so a caller cannot supply the state and drop the outcome. */ settled( threadId: string, turnId: string, - state: 'completed' | 'interrupted', - completedAt: number, - durationMs: number | null = null + terminal: { + state: 'completed' | 'interrupted' + completedAt: number + /** Null when Codex named no verdict, or when the host inferred this end itself. */ + outcome?: AgentJournalTurnOutcome | null + durationMs?: number | null + } ): AgentJournalTurnLifecycle { const startedAt = this.deps.activeTurns.startedAt(threadId, turnId) // Carried forward from the exact echoed send that was attributed to this turn. const requestOrigin = this.deps.activeTurns.requestOrigin(threadId, turnId) return { turnId, - state, + state: terminal.state, + ...(terminal.outcome ? { outcome: terminal.outcome } : {}), userItemId: requestOrigin?.userItemId ?? codexTurnUserItemId(threadId, turnId), ...(startedAt !== undefined ? { startedAt } : {}), ...(requestOrigin !== undefined ? { requestedAt: requestOrigin.requestedAt } : {}), - completedAt, - ...(durationMs !== null ? { durationMs } : {}) + completedAt: terminal.completedAt, + ...(terminal.durationMs != null ? { durationMs: terminal.durationMs } : {}) } } diff --git a/src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts b/src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts index f9cbda202a7..6fc1e88d184 100644 --- a/src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts +++ b/src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts @@ -7,6 +7,10 @@ import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types' import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' +import { + readAgentJournalTurn, + readAgentJournalTurnOutcome +} from '../../shared/agent-session-turn-record' import { createTrackedJournalOpener } from '../native-chat/agent-session-journal/journal-store-test-open' import { createDeferredStructuredAgentSessionEventSink, @@ -310,6 +314,44 @@ describe('codex turn lifecycle rows', () => { }) }) + it('keeps the verdict when a send echoed after completion revises the settled row', () => { + const tap = recorder() + const translator = createCodexJournalTranslator({ + sink: tap.sink, + sessionId: SESSION_ID, + primaryThreadId: () => THREAD_ID, + dispatchRequestOrigin: () => ({ requestedAt: 900, sequence: 0 }) + }) + + translator.handle(notification('turn/started', { turn: { id: TURN_ID } }, 1_000)) + translator.handle( + notification('turn/completed', { turn: { id: TURN_ID, status: 'failed' } }, 2_000) + ) + // The echo lands after the turn settled, so the revision is rebuilt from the + // remembered terminal row. A rebuild that named only the state would drop the + // verdict and leave the failure looking like an ordinary finished turn. + translator.handle( + notification( + 'item/started', + { + turn: { id: TURN_ID }, + item: { type: 'userMessage', id: 'user-1', clientId: 'client-1' } + }, + 2_100 + ) + ) + + // `requestedAt` proves this is the post-echo revision: the terminal row + // written at turn/completed had no request origin to carry yet. + const lifecycle = reduced(tap.rows).find((row) => row.key === LIFECYCLE_KEY) + expect(lifecycle?.body).toMatchObject({ + kind: 'turn', + state: 'interrupted', + outcome: 'failure', + requestedAt: 900 + }) + }) + it('carries the provider duration and the same user item onto the terminal row', () => { const tap = recorder() const translator = translatorFor(tap) @@ -329,6 +371,7 @@ describe('codex turn lifecycle rows', () => { kind: 'turn', turnId: TURN_ID, state: 'completed', + outcome: 'success', userItemId: USER_ITEM_ID, startedAt: 1_000, completedAt: 4_500, @@ -337,9 +380,18 @@ describe('codex turn lifecycle rows', () => { }) }) - it.each(['interrupted', 'failed', 'cancelled'])( - 'maps a %s turn status to an interrupted lifecycle', - (status) => { + // `TurnStatus` in the app-server protocol is `completed | interrupted | failed | + // inProgress`, and every one of those collapses to the same terminal lifecycle + // arm. `outcome` is what keeps a Codex failure distinguishable from a stop, and + // a status this build cannot place stays unknown rather than borrowing one. + it.each([ + ['interrupted', 'cancellation'], + ['failed', 'failure'], + ['cancelled', undefined], + ['inProgress', undefined] + ] as const)( + 'maps a %s turn status to an interrupted lifecycle with outcome %s', + (status, outcome) => { const tap = recorder() const translator = translatorFor(tap) @@ -354,6 +406,7 @@ describe('codex turn lifecycle rows', () => { kind: 'turn', turnId: TURN_ID, state: 'interrupted', + ...(outcome ? { outcome } : {}), userItemId: USER_ITEM_ID, startedAt: 1_000, completedAt: 2_000 @@ -363,6 +416,21 @@ describe('codex turn lifecycle rows', () => { } ) + it('records no outcome for a turn end that named no status', () => { + const tap = recorder() + const translator = translatorFor(tap) + + translator.handle(notification('turn/started', { turn: { id: TURN_ID } }, 1_000)) + // `status` is required on Codex's `Turn`, so its absence is a payload this + // host did not get. The lifecycle still has to name an arm; the verdict does + // not, and inventing `success` here is what a notification would fire on. + translator.handle(notification('turn/completed', { turn: { id: TURN_ID } }, 2_000)) + + const body = reduced(tap.rows).at(-1)?.body + expect(body).toMatchObject({ kind: 'turn', state: 'completed' }) + expect(readAgentJournalTurnOutcome(readAgentJournalTurn(body))).toBeNull() + }) + it('stamps the host clock when a boundary arrives without a receipt time', () => { const tap = recorder() let clock = 10_000 @@ -458,6 +526,7 @@ describe('codex turn lifecycle rows', () => { kind: 'turn', turnId: 'turn-done', state: 'completed', + outcome: 'success', userItemId: 'codex:thread-abc:turn-done:0', startedAt: 1_700_000_000_000, completedAt: 1_700_000_042_000, @@ -470,6 +539,7 @@ describe('codex turn lifecycle rows', () => { kind: 'turn', turnId: 'turn-cut', state: 'interrupted', + outcome: 'cancellation', userItemId: 'codex:thread-abc:turn-cut:0', startedAt: 1_700_000_100_000, completedAt: 1_700_000_101_000 diff --git a/src/main/codex/codex-structured-journal-translation-turns.ts b/src/main/codex/codex-structured-journal-translation-turns.ts index 0e8cddf26ee..651984a72f7 100644 --- a/src/main/codex/codex-structured-journal-translation-turns.ts +++ b/src/main/codex/codex-structured-journal-translation-turns.ts @@ -2,7 +2,8 @@ import type { AgentJournalItemIdentity, AgentJournalTurnItem, AgentJournalTurnLifecycle, - AgentJournalTurnLifecycleState + AgentJournalTurnLifecycleState, + AgentJournalTurnOutcome } from '../../shared/agent-session-journal-types' import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' import { agentJournalTurnBody } from '../../shared/agent-session-turn-record' @@ -49,6 +50,32 @@ export function codexTurnLifecycleState( return status === null || status === 'completed' ? 'completed' : 'interrupted' } +/** + * Codex's own verdict on a turn, or null when this host cannot place one. + * + * `TurnStatus` in the app-server protocol is `completed | interrupted | failed | + * inProgress` and `status` is REQUIRED on every `Turn`, on the live notification + * and in resumed history alike. So a missing or unrecognised status is a payload + * this host did not get, not a clean finish — unlike `codexTurnLifecycleState`, + * which still has to name a terminal lifecycle arm for the row. `inProgress` on + * a turn-end contradicts itself and is no verdict either. + */ +export function codexTurnOutcome(status: string | null): AgentJournalTurnOutcome | null { + switch (status) { + case 'completed': + return 'success' + case 'failed': + return 'failure' + case 'interrupted': + return 'cancellation' + // The fourth protocol arm, plus the two shapes that are no verdict at all. + case 'inProgress': + case null: + default: + return null + } +} + export function publishCodexTurnLifecycle(input: { sink: StructuredAgentSessionEventSink primaryThreadId: string | null @@ -56,6 +83,8 @@ export function publishCodexTurnLifecycle(input: { threadId: string turnId: string state: AgentJournalTurnLifecycleState + /** Absent whenever Codex named no verdict, which reads as unknown. */ + outcome?: AgentJournalTurnOutcome userItemId?: string startedAt?: number requestedAt?: number @@ -69,6 +98,7 @@ export function publishCodexTurnLifecycle(input: { const body = codexTurnLifecycleBody({ turnId: input.turnId, state: input.state, + ...(input.outcome !== undefined ? { outcome: input.outcome } : {}), userItemId: input.userItemId ?? codexTurnUserItemId(input.threadId, input.turnId), ...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}), ...(input.requestedAt !== undefined ? { requestedAt: input.requestedAt } : {}), diff --git a/src/main/codex/codex-structured-journal-translation.ts b/src/main/codex/codex-structured-journal-translation.ts index 0629b87321f..669316b9aee 100644 --- a/src/main/codex/codex-structured-journal-translation.ts +++ b/src/main/codex/codex-structured-journal-translation.ts @@ -162,13 +162,13 @@ export function createCodexJournalTranslator( currentTurnIds: activeTurns.byThread, primaryThreadId: deps.primaryThreadId?.() ?? null, ordinals: items.ordinals, + // The host saw the child go, not what Codex made of the turn, so the row + // carries no outcome: the end is observed, the verdict is unknown. settledTurnLifecycle: (threadId, turnId) => - turnBoundaries.settled( - threadId, - turnId, - 'interrupted', - event.observedAt ?? deps.now?.() ?? Date.now() - ) + turnBoundaries.settled(threadId, turnId, { + state: 'interrupted', + completedAt: event.observedAt ?? deps.now?.() ?? Date.now() + }) }) if (!admission.accepted) { return admission 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..3fc255b60ec 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' @@ -242,16 +243,29 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => { }) describe('background stream thinning compatibility', () => { - it('reports authoritative snapshot support only for the corrected serializer protocol', () => { + it('reports authoritative snapshot support only for the corrected serializer protocol', async () => { + const { id } = await adapter.spawn({ cols: 80, rows: 24 }) const legacy = new DaemonPtyAdapter({ socketPath, tokenPath, protocolVersion: 31 }) try { - expect(legacy.canProvideAuthoritativeBufferSnapshot('legacy-session')).toBe(false) - expect(adapter.canProvideAuthoritativeBufferSnapshot('current-session')).toBe(true) + expect(legacy.canProvideAuthoritativeBufferSnapshot(id)).toBe(false) + expect(adapter.canProvideAuthoritativeBufferSnapshot(id)).toBe(true) } finally { legacy.dispose() } }) + // Why this matters beyond tidiness: getProviderForPty falls back to the local provider for + // any id it cannot place, so these ids reach this adapter for real. Answered from the + // protocol flag alone they came back `true`, which the renderer caches as a definitive + // per-pty licence to unmount a pane whose bytes this daemon never held. + it('refuses an authoritative snapshot claim for a session it does not own', async () => { + const { id } = await adapter.spawn({ cols: 80, rows: 24 }) + + expect(adapter.canProvideAuthoritativeBufferSnapshot(id)).toBe(true) + expect(adapter.canProvideAuthoritativeBufferSnapshot('remote:env-1:pty-1')).toBe(false) + expect(adapter.canProvideAuthoritativeBufferSnapshot('never-spawned-session')).toBe(false) + }) + it('reports background state on the authoritative-snapshot protocol', () => { const notifySpy = vi.spyOn(DaemonClient.prototype, 'notify') try { 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-runtime-state.ts b/src/main/daemon/daemon-pty-runtime-state.ts index e471f698748..38480af1489 100644 --- a/src/main/daemon/daemon-pty-runtime-state.ts +++ b/src/main/daemon/daemon-pty-runtime-state.ts @@ -237,8 +237,12 @@ export abstract class DaemonPtyRuntimeState { return this.protocolVersion >= GIT_CREDENTIAL_GUARD_HOST_PROTOCOL_VERSION } - canProvideAuthoritativeBufferSnapshot(_id: string): boolean { - return this.supportsAuthoritativeBufferSnapshots + // Why the id is read rather than ignored: the contract promises a fact about THIS pty, and + // getProviderForPty falls back to the local provider for any id it cannot place. A + // remote-runtime id therefore reaches this adapter, and answering from the protocol flag + // alone returned `true` for a session this daemon has never owned. + canProvideAuthoritativeBufferSnapshot(id: string): boolean { + return this.supportsAuthoritativeBufferSnapshots && this.activeSessionIds.has(id) } protected get canDelegateBackgroundToDaemon(): boolean { 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-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-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-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() + }) +}) 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/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 }) }) 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/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/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-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-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-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-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-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') 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/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/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) +}) 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() diff --git a/src/main/ipc/codex-accounts.ts b/src/main/ipc/codex-accounts.ts index e89e8bca4be..4d828ecf82b 100644 --- a/src/main/ipc/codex-accounts.ts +++ b/src/main/ipc/codex-accounts.ts @@ -1,6 +1,7 @@ import { ipcMain } from 'electron' import type { CodexAccountAddTarget, CodexAccountService } from '../codex-accounts/service' import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection' +import { broadcastCodexPendingLoginUrl } from './codex-pending-login-url-broadcast' import { listRecordedCodexPaneLanes } from '../codex/codex-pane-account-registry' import { forgetStaleCodexPanes, listStaleCodexPanes } from '../codex/codex-stale-pane-accounts' import type { GlobalSettings } from '../../shared/global-settings-types' @@ -38,6 +39,11 @@ export function registerCodexAccountHandlers( ipcMain.handle('codexAccounts:add', (_event, args?: CodexAccountAddTarget) => codexAccounts.addAccount(args) ) + ipcMain.handle('codexAccounts:cancelPendingLogin', () => codexAccounts.cancelPendingLogin()) + ipcMain.handle('codexAccounts:pendingLoginUrl', () => codexAccounts.getPendingLoginUrl()) + // Why: Settings can open after the login already printed its link, so the + // renderer reads the current value on mount and this only carries changes. + codexAccounts.onPendingLoginUrlChanged(broadcastCodexPendingLoginUrl) ipcMain.handle( 'codexAccounts:reauthenticate', (_event, args: { accountId: string; activateIfSelectionWasEmpty?: boolean }) => diff --git a/src/main/ipc/codex-pending-login-url-broadcast.ts b/src/main/ipc/codex-pending-login-url-broadcast.ts new file mode 100644 index 00000000000..5599eb5f968 --- /dev/null +++ b/src/main/ipc/codex-pending-login-url-broadcast.ts @@ -0,0 +1,15 @@ +import { BrowserWindow } from 'electron' +import { CODEX_PENDING_LOGIN_URL_CHANGED_CHANNEL } from '../../shared/codex-auth-errors' + +export function broadcastCodexPendingLoginUrl(url: string | null): void { + for (const window of BrowserWindow.getAllWindows()) { + if (window.isDestroyed()) { + continue + } + try { + window.webContents.send(CODEX_PENDING_LOGIN_URL_CHANGED_CHANNEL, url) + } catch { + // A renderer can disappear between isDestroyed() and send(). + } + } +} 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. 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/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: ( 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/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() + } + }) +}) 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-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/native-chat/agent-session-wire/structured-agent-session-launch-send-after-create.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-launch-send-after-create.test.ts new file mode 100644 index 00000000000..300f1423cb3 --- /dev/null +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-launch-send-after-create.test.ts @@ -0,0 +1,96 @@ +// Where a launch prompt sent the instant `attach` resolves reaches the adapter. +// +// Both shipped adapters look the session up in a live map and throw when it is absent +// (`claude-structured-session-adapter.ts:331-337`, `codex-structured-session-state.ts`'s +// `requireLiveCodexSession`), and both populate that map as the last step of `acquire` +// (`claude-structured-session-acquisition.ts:278`, `codex-structured-session-acquire.ts:275`). +// So the question is purely one of ordering, and that is what these model. + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { StructuredAgentSessionHost } from './structured-agent-session-host' +import { commitStructuredAgentSessionLaunchPrompt } from '../../runtime/rpc/methods/agent-launch-structured-prompt' +import { + accepted, + attachParams, + CALLER, + hostTestState +} from './structured-agent-session-host-test-harness' +import { HOST_TEST_NOW as NOW } from './structured-agent-session-host-test-data' + +let host: StructuredAgentSessionHost + +/** Mirrors both adapters: `acquire` publishes into the live map, `dispatch` throws without it. */ +function modelAdapterLiveness(registerOnAcquire: boolean): void { + const { acquire, dispatch } = hostTestState() + const live = new Set() + const spawn = acquire.getMockImplementation()! + acquire.mockImplementation(async (input) => { + const acquisition = await spawn(input) + if (registerOnAcquire) { + live.add(input.identity.sessionId) + } + return acquisition + }) + dispatch.mockImplementation(async ({ sessionId }) => { + if (!live.has(sessionId)) { + throw new Error(`no live structured session for ${sessionId}`) + } + return accepted() + }) +} + +/** Exactly what `agentLaunchSurfaceFactory` does: create, then send on the create's own fence. */ +async function launchAndDeliver(): Promise<{ + messageId: string | null + dispatchState: string +}> { + const send = vi.spyOn(host, 'send') + const created = await host.attach(CALLER, attachParams()) + if (!created.ok) { + throw new Error(`expected a create, got ${created.refusal.code}`) + } + const messageId = await commitStructuredAgentSessionLaunchPrompt({ + host, + caller: CALLER, + sessionId: created.value.sessionId, + fence: created.value.fence, + text: 'fix the failing test' + }) + const sent = await send.mock.results[0]!.value + return { + messageId, + dispatchState: sent.ok + ? sent.value.submission.dispatchState + : `refused:${sent.refusal.code}:${sent.refusal.message}` + } +} + +beforeEach(() => { + ;({ host } = hostTestState()) + // The harness pins the host clock; operation ids are minted from `Date.now()` and carry a + // timestamp the host expires against, so the two must agree or every send reads as stale. + vi.spyOn(Date, 'now').mockReturnValue(NOW) +}) + +describe('a launch prompt sent the instant the create resolves', () => { + it('reaches a live provider, because attach returns only after acquire published it', async () => { + modelAdapterLiveness(true) + + await expect(launchAndDeliver()).resolves.toEqual({ + messageId: expect.any(String), + dispatchState: 'accepted' + }) + expect(hostTestState().dispatch).toHaveBeenCalledTimes(1) + }) + + // Positive control: the assertion above is only evidence if this arm can fail, and it names + // the user-visible symptom precisely — a committed row that no agent will ever answer. + it('would commit a row and strand it if acquire ever resolved before publication', async () => { + modelAdapterLiveness(false) + + await expect(launchAndDeliver()).resolves.toEqual({ + messageId: expect.any(String), + dispatchState: 'unknown' + }) + }) +}) diff --git a/src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts b/src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts index ac13c75d480..6fa4d32293b 100644 --- a/src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts @@ -87,6 +87,60 @@ describe('rewind recovery of newer durable records', () => { text: JSON.stringify(unknown) }) }) + it('keeps a known turn outcome across rewind recovery in both journal shapes', () => { + const turn = { + kind: 'turn' as const, + turnId: 'turn', + state: 'completed', + outcome: 'failure', + userItemId: 'codex:thread:turn:0', + startedAt: 10, + completedAt: 20 + } + expect(restoreRewindJournalBody(turn)).toEqual(turn) + const status = { + kind: 'status' as const, + text: 'Claude turn completed', + turnLifecycle: { turnId: 'turn', state: 'interrupted', outcome: 'cancellation' } + } + expect(restoreRewindJournalBody(status)).toEqual(status) + }) + + it('drops a turn outcome from a later vocabulary but keeps the turn and its endpoints', () => { + // An unknown outcome costs nothing to discard, because absent already means + // unknown. Falling back to a status row the way an unknown STATE does would + // take the turn's endpoints with it, and every timing surface reads those. + expect( + restoreRewindJournalBody({ + kind: 'turn', + turnId: 'turn', + state: 'completed', + outcome: 'partially-refused', + userItemId: 'codex:thread:turn:0', + startedAt: 10, + completedAt: 20 + }) + ).toEqual({ + kind: 'turn', + turnId: 'turn', + state: 'completed', + userItemId: 'codex:thread:turn:0', + startedAt: 10, + completedAt: 20 + }) + expect( + restoreRewindJournalBody({ + kind: 'status', + text: 'Codex turn completed', + turnLifecycle: { turnId: 'turn', state: 'completed', outcome: 'partially-refused' } + }) + ).toEqual({ + kind: 'status', + text: 'Codex turn completed', + turnLifecycle: { turnId: 'turn', state: 'completed' } + }) + }) + it('does not reject a saved recovery prefix over a newer refusal reason', () => { expect( AgentSessionRewindRecordSchema.safeParse({ diff --git a/src/main/native-chat/agent-session-wire/structured-rewind-journal-body.ts b/src/main/native-chat/agent-session-wire/structured-rewind-journal-body.ts index db92d55e110..addc234e2a1 100644 --- a/src/main/native-chat/agent-session-wire/structured-rewind-journal-body.ts +++ b/src/main/native-chat/agent-session-wire/structured-rewind-journal-body.ts @@ -1,6 +1,7 @@ import { isAdmissibleAgentJournalItemBody } from '../../../shared/agent-session-journal-schemas' import { AGENT_JOURNAL_TURN_LIFECYCLE_STATES, + AGENT_JOURNAL_TURN_OUTCOMES, type AgentJournalItemBody } from '../../../shared/agent-session-journal-types' import type { AgentSessionRewindRecord } from '../../../shared/agent-session-rewind' @@ -61,9 +62,36 @@ export function restoreRewindJournalBody(body: StoredBody): AgentJournalItemBody ) ) { normalized = fallback() + } else if (body.kind === 'turn' || (body.kind === 'status' && body.turnLifecycle)) { + normalized = withKnownTurnOutcome(body) } if (!isAdmissibleAgentJournalItemBody(normalized)) { throw new Error('agent_session_rewind:invalid-retained-body') } return normalized } + +/** A verdict from a later vocabulary is dropped, never coerced and never fatal. + * Unlike an unknown `state`, an unplaceable outcome costs nothing to discard — + * absent already means unknown — and discarding it keeps the turn's endpoints, + * which a status fallback would throw away along with the timing every surface + * reads. */ +function withKnownTurnOutcome( + body: Extract +): StoredBody { + const known = (outcome: string | undefined): boolean => + outcome === undefined || AGENT_JOURNAL_TURN_OUTCOMES.some((arm) => arm === outcome) + if (body.kind === 'turn') { + if (known(body.outcome)) { + return body + } + const { outcome: _outcome, ...rest } = body + return rest + } + const lifecycle = body.turnLifecycle + if (!lifecycle || known(lifecycle.outcome)) { + return body + } + const { outcome: _outcome, ...rest } = lifecycle + return { ...body, turnLifecycle: rest } +} 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) +}) 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/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 } 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/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) + }) +} 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/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/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/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-get-status.ts b/src/main/runtime/orca-runtime-get-status.ts index bf7d3818045..f3d16bd1f4e 100644 --- a/src/main/runtime/orca-runtime-get-status.ts +++ b/src/main/runtime/orca-runtime-get-status.ts @@ -21,6 +21,8 @@ import { BROWSER_UNAVAILABLE_ERROR_CODE, browserUnavailableMessage } from '../../shared/runtime-types' +import { MOBILE_WEB_BUNDLE_CAPABILITY } from '../../shared/mobile-web-bundle/mobile-web-bundle-capability' +import { loadBundledMobileWebBundle } from './bundled-mobile-web-bundle' import { runtimeTerminalDegradation } from './native-terminal-availability' import { isWindowsProcessStartTimeAvailable } from '../windows/windows-process-table' import type { RuntimeWorktreeLifecycleEvent } from './orca-runtime-core' @@ -89,6 +91,12 @@ export class OrcaRuntimeWithGetStatus extends OrcaRuntimeWithGetRuntimeId { if (canBrowse) { capabilities.push(BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY) } + // Why not a static capability: dev trees and `orca serve` installs may carry no + // out/mobile-web, and advertising a bundle this install cannot produce would promise a + // download that only ever answers mobile_web_bundle_unavailable. + if (loadBundledMobileWebBundle()) { + capabilities.push(MOBILE_WEB_BUNDLE_CAPABILITY) + } // Why the cause and not one fixed sentence: the operator can only act on the reason // that actually applies, and a host that says "set ORCA_BROWSER_EXECUTABLE" to someone // who already set it sends them to fix a thing that is not broken. diff --git a/src/main/runtime/orca-runtime-list-managed-worktrees.ts b/src/main/runtime/orca-runtime-list-managed-worktrees.ts index 69aa37b6ef0..7fe067e9276 100644 --- a/src/main/runtime/orca-runtime-list-managed-worktrees.ts +++ b/src/main/runtime/orca-runtime-list-managed-worktrees.ts @@ -8,6 +8,7 @@ import { stopMissingWorktreeTerminals } from './missing-worktree-terminal-reconc import type { RuntimeCommandSurfaceHost } from './orca-runtime-core' import type { WorktreeVisibilitySourceMatcher } from '../../shared/worktree/visibility-sources' import type { RuntimeStore } from './runtime-store-contract' +import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' import type { WorkspacePortKillRequest, WorkspacePortKillResult, @@ -118,6 +119,10 @@ export class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWithRestoreS return await this.resolveWorktreeSelector(worktreeSelector) } + /** + * The git worktree record behind a terminal workspace. Refuses the floating sentinel, which has + * no such record — callers that only need to address the workspace want the scope below instead. + */ async showManagedTerminalWorkspace(worktreeSelector: string) { const target = await this.resolveTerminalWorkspaceLaunchTarget(worktreeSelector) if (!target.managedWorktree) { @@ -126,6 +131,18 @@ export class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWithRestoreS return target.managedWorktree } + /** + * Where a terminal workspace is, for every kind one can be: a git worktree, a folder workspace, + * or the floating sentinel. This is the general answer — `id` and `path` are resolved the same + * way for all three — so a caller that reads only those must ask for this rather than demand a + * worktree record it never reads and lose the floating workspace to a `selector_not_found`. + */ + async showTerminalWorkspaceLaunchScope( + worktreeSelector: string + ): Promise { + return await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector) + } + async scanWorkspacePorts(repoId?: string): Promise { return scanWorkspacePortProbes(await this.getWorkspacePortProbes(repoId)) } 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-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-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-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-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) 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/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/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/orca-runtime-tests/mobile-web-bundle-capability.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-web-bundle-capability.spec.ts new file mode 100644 index 00000000000..333ff8cabc8 --- /dev/null +++ b/src/main/runtime/orca-runtime-tests/mobile-web-bundle-capability.spec.ts @@ -0,0 +1,65 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { createRuntime } from '../orca-runtime-test-fixtures.spec' +import { resetBundledMobileWebBundleCacheForTests } from '../bundled-mobile-web-bundle' +import { + installMobileWebBundleAppPath, + writeSyntheticMobileWebBundle +} from '../rpc/methods/mobile-web-bundle.test-fixture' +import { MOBILE_WEB_BUNDLE_CAPABILITY } from '../../../shared/mobile-web-bundle/mobile-web-bundle-capability' + +// The resolver caches per process and the aggregated suite shares that process, so each case +// installs its own root and hands the cache back the way it found it. +function statusCapabilitiesForInstall(appPath: string): readonly string[] { + resetBundledMobileWebBundleCacheForTests() + installMobileWebBundleAppPath(appPath) + const { capabilities } = createRuntime().getStatus() + if (!capabilities) { + // A status carrying no list at all would let every absence assertion below pass vacuously. + throw new Error('status.get answered no capability list') + } + return capabilities +} + +function withInstallRoot(prefix: string, run: (install: string) => void): void { + const install = mkdtempSync(join(tmpdir(), prefix)) + try { + run(install) + } finally { + rmSync(install, { recursive: true, force: true }) + } +} + +describe('OrcaRuntimeService mobile web bundle capability', () => { + afterEach(() => { + resetBundledMobileWebBundleCacheForTests() + }) + + // The mixed-version guarantee: a dev tree or an `orca serve` install that never built + // out/mobile-web must not promise a bundle it can only refuse. + it('omits the mobile web bundle capability where the install carries no bundle', () => { + withInstallRoot('orca-mobile-web-absent-', (install) => { + expect(statusCapabilitiesForInstall(install)).not.toContain(MOBILE_WEB_BUNDLE_CAPABILITY) + }) + }) + + it('advertises the mobile web bundle capability where the install carries one', () => { + withInstallRoot('orca-mobile-web-present-', (install) => { + writeSyntheticMobileWebBundle(join(install, 'out', 'mobile-web'), 4) + expect(statusCapabilitiesForInstall(install)).toContain(MOBILE_WEB_BUNDLE_CAPABILITY) + }) + }) + + // A manifest the contract rejects is the same answer as no bundle, so advertising off the + // directory's existence rather than off a parsed manifest would promise a download that errors. + it('omits the capability where the bundle manifest does not parse', () => { + withInstallRoot('orca-mobile-web-unparseable-', (install) => { + const root = join(install, 'out', 'mobile-web') + writeSyntheticMobileWebBundle(root, 5) + writeFileSync(join(root, 'manifest.json'), '{"schemaVersion":2}', 'utf8') + expect(statusCapabilitiesForInstall(install)).not.toContain(MOBILE_WEB_BUNDLE_CAPABILITY) + }) + }) +}) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index d9e187817bf..13796169be0 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -11,6 +11,7 @@ await import('./orca-runtime-tests/paired-settings.spec') await import('./orca-runtime-tests/runtime-availability.spec') await import('./orca-runtime-tests/browser-capabilities.spec') await import('./orca-runtime-tests/browser-capabilities-part-02.spec') +await import('./orca-runtime-tests/mobile-web-bundle-capability.spec') await import('./orca-runtime-tests/window-authority.spec') await import('./orca-runtime-tests/terminal-handles.spec') await import('./orca-runtime-tests/terminal-handles-part-02.spec') 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' }) + }) +}) 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/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/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/src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts b/src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts new file mode 100644 index 00000000000..5551c4de354 --- /dev/null +++ b/src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts @@ -0,0 +1,79 @@ +import { homedir } from 'node:os' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' +import { OrcaRuntimeService } from '../../orca-runtime' +import { AGENT_LAUNCH_METHODS } from './agent-launch' +import { CAPABLE_CLIENT, methodNamed, STRUCTURED_PREFERENCE } from './agent-launch.test-fixture' + +vi.mock('electron', () => ({ + BrowserWindow: { fromId: vi.fn(() => null) }, + webContents: { fromId: vi.fn(() => null) }, + ipcMain: { on: vi.fn(), removeListener: vi.fn() }, + app: { getPath: vi.fn(() => '/tmp') } +})) + +const launch = methodNamed(AGENT_LAUNCH_METHODS, 'agent.launch') +const selectors = [FLOATING_TERMINAL_WORKTREE_ID, `id:${FLOATING_TERMINAL_WORKTREE_ID}`] + +afterEach(() => vi.restoreAllMocks()) + +describe('agent.launch with the real floating workspace resolver', () => { + it.each(selectors)('resolves %s without a managed worktree record', async (selector) => { + const runtime = new OrcaRuntimeService() + + await expect(runtime.showManagedTerminalWorkspace(selector)).rejects.toThrow( + 'selector_not_found' + ) + await expect(runtime.showTerminalWorkspaceLaunchScope(selector)).resolves.toEqual({ + id: FLOATING_TERMINAL_WORKTREE_ID, + path: homedir(), + connectionId: null, + repo: null, + folderWorkspace: null + }) + }) + + describe.each([true, false])('structured preference %s', (structuredPreference) => { + it.each(selectors)('launches a terminal through %s', async (selector) => { + const runtime = new OrcaRuntimeService() + vi.spyOn(runtime, 'getClientSettings').mockReturnValue( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the launch reads only these preferences and optional agentCmdOverrides; no other settings consumer runs because terminal creation is stubbed. + { + ...STRUCTURED_PREFERENCE, + openAgentTabsInChatByDefault: structuredPreference + } as ReturnType + ) + const scope = vi.spyOn(runtime, 'showTerminalWorkspaceLaunchScope') + const createSupport = vi.spyOn(runtime, 'getStructuredAgentSessionCreateSupport') + const structuredHost = vi.spyOn(runtime, 'ensureStructuredAgentSessionHost') + const createTerminal = vi.spyOn(runtime, 'createTerminal').mockResolvedValue({ + handle: 'term_floating', + tabId: 'tab_floating', + worktreeId: FLOATING_TERMINAL_WORKTREE_ID, + title: 'Claude', + surface: 'background' + }) + + const result = await launch.handler( + launch.params.parse({ agent: 'claude', target: { kind: 'existing', worktree: selector } }), + { runtime, ...CAPABLE_CLIENT } + ) + + expect(scope).toHaveBeenCalledExactlyOnceWith(selector) + expect(createSupport).not.toHaveBeenCalled() + expect(structuredHost).not.toHaveBeenCalled() + expect(createTerminal).toHaveBeenCalledExactlyOnceWith( + `id:${FLOATING_TERMINAL_WORKTREE_ID}`, + { startupAgent: 'claude' } + ) + expect(result).toMatchObject({ + worktreeId: FLOATING_TERMINAL_WORKTREE_ID, + outcome: { kind: 'terminal', handle: 'term_floating' }, + receipt: { + mode: 'terminal', + reason: structuredPreference ? 'structured_unsupported_on_host' : 'user_default' + } + }) + }) + }) +}) diff --git a/src/main/runtime/rpc/methods/agent-launch-replay.test.ts b/src/main/runtime/rpc/methods/agent-launch-replay.test.ts index 6787bcfcf10..3d6981ed4a1 100644 --- a/src/main/runtime/rpc/methods/agent-launch-replay.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch-replay.test.ts @@ -339,7 +339,7 @@ describe('an uncertain launch stays uncertain', () => { it('records a failure that happened before anything could be created', async () => { const runtime = runtimeStub() - runtime.showManagedTerminalWorkspace.mockRejectedValueOnce(new Error('worktree_not_found')) + runtime.showTerminalWorkspaceLaunchScope.mockRejectedValueOnce(new Error('worktree_not_found')) await expect( launch( @@ -481,7 +481,7 @@ describe('an unreadable launch payload costs one replay, never the store', () => describe('a recorded failure replays as the failure it was', () => { it('answers with the code the launch actually raised, not the ledger vocabulary', async () => { const runtime = runtimeStub() - runtime.showManagedTerminalWorkspace.mockRejectedValue(new Error('worktree_not_found')) + runtime.showTerminalWorkspaceLaunchScope.mockRejectedValue(new Error('worktree_not_found')) const params = createLaunch({ operationId: OPERATION_ID, target: { kind: 'existing', worktree: 'gone' } @@ -493,14 +493,14 @@ describe('a recorded failure replays as the failure it was', () => { // malformed" signal, which tells a client to mint a fresh id when the truthful answer is that // this launch definitively did not run. const replayed = runtimeStub() - replayed.showManagedTerminalWorkspace.mockRejectedValue(new Error('worktree_not_found')) + replayed.showTerminalWorkspaceLaunchScope.mockRejectedValue(new Error('worktree_not_found')) await expect(launch(params, replayed)).rejects.toThrow('worktree_not_found') - expect(replayed.showManagedTerminalWorkspace).not.toHaveBeenCalled() + expect(replayed.showTerminalWorkspaceLaunchScope).not.toHaveBeenCalled() }) it('bounds the code it persists, because a code is an identifier and a message is not', async () => { const runtime = runtimeStub() - runtime.showManagedTerminalWorkspace.mockRejectedValue( + runtime.showTerminalWorkspaceLaunchScope.mockRejectedValue( new Error(`ENOENT: no such file or directory, stat '${'/very/long/path'.repeat(400)}'`) ) diff --git a/src/main/runtime/rpc/methods/agent-launch-structured-prompt.test.ts b/src/main/runtime/rpc/methods/agent-launch-structured-prompt.test.ts new file mode 100644 index 00000000000..4d919df556e --- /dev/null +++ b/src/main/runtime/rpc/methods/agent-launch-structured-prompt.test.ts @@ -0,0 +1,96 @@ +/** + * The commit half of launch-prompt delivery: what the host sends, and what it is willing to claim. + * + * The assertions that matter are that the send is shaped like every other client's — the entry's + * operation id IS the client message id, and the fingerprint is computed over the same body — and + * that no failure mode can return an id, because an id is what the caller reads as "committed". + */ + +import { describe, expect, it, vi } from 'vitest' +import { commitStructuredAgentSessionLaunchPrompt } from './agent-launch-structured-prompt' +import { structuredAgentSessionPayloadFingerprint } from '../../../../shared/structured-agent-session-mutation' +import { structuredAgentSessionSendBody } from '../../../../shared/structured-agent-session-outbox' +import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host' + +const CALLER = { callerKey: 'trusted-local:runtime' } + +function hostWith( + send: ReturnType, + journalSnapshot: ReturnType = vi.fn(() => ({ submissions: [] })) +): StructuredAgentSessionHost { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub implements the only method this module reaches; any other would throw rather than read a wrong value. + return { send, journalSnapshot } as unknown as StructuredAgentSessionHost +} + +function commit(host: StructuredAgentSessionHost | null, text = 'do the thing') { + return commitStructuredAgentSessionLaunchPrompt({ + host, + caller: CALLER, + sessionId: 'sess-1', + fence: 4, + text + }) +} + +describe('committing a launch prompt', () => { + it('sends the entry as its own client message id and names the committed row', async () => { + const send = vi.fn(async (_caller, params) => ({ + ok: true as const, + value: { clientMessageId: params.envelope.clientOperationId, submission: {} } + })) + + const messageId = await commit(hostWith(send)) + + const [caller, params] = send.mock.calls[0] + expect(caller).toEqual(CALLER) + expect(messageId).toBe(params.envelope.clientOperationId) + expect(params.envelope).toMatchObject({ sessionId: 'sess-1', expectedRuntimeFence: 4 }) + expect(params.body).toEqual(structuredAgentSessionSendBody('do the thing', [])) + // The host recomputes and compares this, so a launch send must fingerprint like a client send. + expect(params.envelope.payloadFingerprint).toBe( + structuredAgentSessionPayloadFingerprint({ + method: 'agentSession.send', + sessionId: 'sess-1', + fields: { body: params.body } + }) + ) + }) + + it('claims nothing when the send is refused', async () => { + const send = vi.fn(async () => ({ + ok: false as const, + refusal: { code: 'agent_session_operation_invalid', message: 'no' } + })) + await expect(commit(hostWith(send))).resolves.toBeNull() + }) + + it('recovers a committed row when settlement throws after append', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + let clientMessageId = '' + const send = vi.fn( + async (_caller: unknown, params: { envelope: { clientOperationId: string } }) => { + clientMessageId = params.envelope.clientOperationId + throw new Error('host gone') + } + ) + const journalSnapshot = vi.fn((_sessionId) => ({ + submissions: [{ clientMessageId }] + })) + await expect(commit(hostWith(send, journalSnapshot))).resolves.toEqual(clientMessageId) + }) + + it('claims nothing, and does not fail the launch, when no row was committed', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + const send = vi.fn(async () => { + throw new Error('host gone') + }) + await expect(commit(hostWith(send))).resolves.toBeNull() + }) + + it('sends nothing when there is no host or no text', async () => { + const send = vi.fn() + await expect(commit(null)).resolves.toBeNull() + await expect(commit(hostWith(send), ' ')).resolves.toBeNull() + expect(send).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/runtime/rpc/methods/agent-launch-structured-prompt.ts b/src/main/runtime/rpc/methods/agent-launch-structured-prompt.ts new file mode 100644 index 00000000000..0037d6fc35e --- /dev/null +++ b/src/main/runtime/rpc/methods/agent-launch-structured-prompt.ts @@ -0,0 +1,78 @@ +/** + * Host-side delivery of a launch's initial text to the structured session the launch just created. + * + * It exists so a caller does not have to implement delivery itself. Before this, `agent.launch` + * created the surface and reported the text as undelivered, which was only workable while the one + * surface that could send — the desktop renderer's chat — was also the one issuing the launch. + * Mobile and anything else calling `agent.launch` got an agent and no prompt. + * + * Nothing here queues. The durable record that the text is owed already exists and is the journal's + * own submission row: `performSend` appends it before dispatching and the attach path settles it, so + * a second host-side copy could only disagree with it. What IS reused is the outbox's entry and + * envelope builders, so this send is shaped exactly like the renderer's and mobile's — same body, + * same operation id as client message id, same payload fingerprint. + * + * The renderer still delivers its own launch prompts, because its launcher does not call + * `agent.launch` yet; when it does, its launch-sourced outbox entries become this call. + */ + +import { + createStructuredAgentSessionOutboxEntry, + structuredAgentSessionSendMutation +} from '../../../../shared/structured-agent-session-outbox' +import { createStructuredAgentSessionOperationId } from '../../../../shared/structured-agent-session-mutation' +import { randomUUID } from 'node:crypto' +import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host' +import type { StructuredAgentSessionCaller } from '../../../native-chat/agent-session-wire/structured-agent-session-host-types' + +/** + * The committed transcript row's id, or `null` when nothing was committed. + * + * `ok` is the host's own proof of the commit. A send can still throw after appending (for example + * when operation settlement fails), so throws are reconciled against the host's journal before we + * under-claim. A resend after that boundary would duplicate the model turn. + * + * Deliberately does NOT wait for the dispatch to settle. The row is committed either way, and + * whether the provider took the turn is the submission's own state to carry. + */ +export async function commitStructuredAgentSessionLaunchPrompt(args: { + host: StructuredAgentSessionHost | null + caller: StructuredAgentSessionCaller + sessionId: string + fence: number + text: string +}): Promise { + if (!args.host || args.text.trim().length === 0) { + return null + } + const clientMessageId = createStructuredAgentSessionOperationId(randomUUID) + const entry = createStructuredAgentSessionOutboxEntry({ + clientMessageId, + sessionId: args.sessionId, + text: args.text, + attachments: [], + queuedAt: Date.now() + }) + try { + const result = await args.host.send( + args.caller, + structuredAgentSessionSendMutation(entry, args.fence) + ) + return result.ok ? result.value.clientMessageId : null + } catch (error) { + // Settlement can fail after the journal append. Re-read the authoritative row before asking + // the caller to resend, otherwise a retry creates a duplicate turn. + try { + const committed = args.host + .journalSnapshot(args.sessionId) + .submissions.find((submission) => submission.clientMessageId === clientMessageId) + if (committed) { + return clientMessageId + } + } catch { + // The host may have gone away before the snapshot; the caller retains the text in that case. + } + console.warn('[agent-launch] the session was created, its launch prompt was not sent', error) + return null + } +} diff --git a/src/main/runtime/rpc/methods/agent-launch-surfaces.ts b/src/main/runtime/rpc/methods/agent-launch-surfaces.ts index ad5cb7c4c86..e610da8285d 100644 --- a/src/main/runtime/rpc/methods/agent-launch-surfaces.ts +++ b/src/main/runtime/rpc/methods/agent-launch-surfaces.ts @@ -6,6 +6,9 @@ * new agent tab is. Orchestration's own factories are NOT reusable here — a worker's session * carries a dispatch hold, a mailbox and a background tab that a launch the user asked for must * not take — which is why the executor injects this rather than branching. + * + * Delivering the launch text is here for the same reason: it is the wire-shaped half, and only the + * structured half has somewhere to commit it to. */ import { randomUUID } from 'node:crypto' @@ -21,6 +24,7 @@ import type { StructuredAgentSessionHost } from '../../../native-chat/agent-sess import type { RpcContext } from '../core' import { structuredCallerFor } from './structured-agent-session-gate' import { createStructuredAgentSessionForWorktree } from './structured-agent-session-create' +import { commitStructuredAgentSessionLaunchPrompt } from './agent-launch-structured-prompt' /** Replay-safe launches keep the nested attach in the same stable caller namespace as the launch. */ export function agentLaunchSurfaceFactory( @@ -65,9 +69,20 @@ export function agentLaunchSurfaceFactory( } return { sessionId: created.value.sessionId, - handle: structuredAgentSessionTabId(created.value.sessionId) + handle: structuredAgentSessionTabId(created.value.sessionId), + fence: created.value.fence } }, + deliverStructuredPrompt: async ({ sessionId, fence, prompt }) => + commitStructuredAgentSessionLaunchPrompt({ + host: getStructuredAgentSessionHost(), + caller: operationCallerKey + ? { callerKey: operationCallerKey } + : structuredCallerFor(context), + sessionId, + fence, + text: prompt.text + }), createTerminalAgent: async ({ worktreeId, agent }) => { const terminal = await context.runtime.createTerminal(`id:${worktreeId}`, { // The agent id is not a shell command — `cursor` is the desktop app, its CLI is diff --git a/src/main/runtime/rpc/methods/agent-launch.test-fixture.ts b/src/main/runtime/rpc/methods/agent-launch.test-fixture.ts index 7272b5bb7af..6b52635fc72 100644 --- a/src/main/runtime/rpc/methods/agent-launch.test-fixture.ts +++ b/src/main/runtime/rpc/methods/agent-launch.test-fixture.ts @@ -73,6 +73,15 @@ export function runtimeStub(options: AgentLaunchRuntimeStubOptions = {}) { showManagedTerminalWorkspace: vi.fn(async (selector: string) => ({ id: selector.replace(/^id:/, '') })), + // The scope resolves for every workspace kind, so unlike the worktree record above it never + // refuses the floating sentinel — which is the whole reason the launch asks for this one. + showTerminalWorkspaceLaunchScope: vi.fn(async (selector: string) => ({ + id: selector.replace(/^id:/, ''), + path: '/tmp/wt-7', + connectionId: null, + repo: null, + folderWorkspace: null + })), ensureStructuredAgentSessionHost: vi.fn(async () => {}), waitForSetupTerminalCompletion } diff --git a/src/main/runtime/rpc/methods/agent-launch.test.ts b/src/main/runtime/rpc/methods/agent-launch.test.ts index 6806726f0d2..2175aab0234 100644 --- a/src/main/runtime/rpc/methods/agent-launch.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch.test.ts @@ -449,7 +449,9 @@ describe('the terminal factory', () => { ) expect(runtime.createManagedWorktree).not.toHaveBeenCalled() - expect(runtime.showManagedTerminalWorkspace).toHaveBeenCalledWith('id:wt-7') + // The scope, not the worktree record: asking for the record refused any workspace without one. + expect(runtime.showTerminalWorkspaceLaunchScope).toHaveBeenCalledWith('id:wt-7') + expect(runtime.showManagedTerminalWorkspace).not.toHaveBeenCalled() // Resolved to an id first: everything below re-prefixes it, so a raw selector reaches the // runtime as `id:id:wt-7`. expect(runtime.createTerminal).toHaveBeenCalledWith('id:wt-7', { startupAgent: 'grok' }) diff --git a/src/main/runtime/rpc/methods/agent-launch.ts b/src/main/runtime/rpc/methods/agent-launch.ts index af6e018a36d..787652fa293 100644 --- a/src/main/runtime/rpc/methods/agent-launch.ts +++ b/src/main/runtime/rpc/methods/agent-launch.ts @@ -59,17 +59,22 @@ export function supportsAgentLaunch( /** * A client addresses a workspace by selector, but the result's `worktreeId` is an id and every * step below the executor re-prefixes it as `id:`. Resolving here is what keeps a - * caller's `id:wt-7` from reaching the runtime as `id:id:wt-7`; the terminal-workspace resolver is - * used rather than the git-worktree one so a folder workspace is addressable too. + * caller's `id:wt-7` from reaching the runtime as `id:id:wt-7`. + * + * The launch *scope* is what is asked for, because the id below is the only thing read off it. The + * git-worktree record is the narrower answer — it does not exist for the floating workspace, so + * asking for one refused a launch this method can perfectly well run, on a workspace whose id it + * had already resolved. A folder workspace survived that only because the resolver fabricates a + * worktree row for it; the scope is the answer that is real for all three kinds. */ async function agentLaunchTarget( params: AgentLaunchParams, - runtime: Pick + runtime: Pick ): Promise { if (params.target.kind === 'create-worktree') { return { kind: 'create-worktree', create: { ...params.target.create } } } - const workspace = await runtime.showManagedTerminalWorkspace(params.target.worktree) + const workspace = await runtime.showTerminalWorkspaceLaunchScope(params.target.worktree) return { kind: 'existing', worktree: workspace.id } } 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/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/src/main/runtime/rpc/methods/structured-agent-session-turn-item-capability.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-turn-item-capability.test.ts index bf7eb50fb23..0393cf216c8 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-turn-item-capability.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-turn-item-capability.test.ts @@ -23,9 +23,13 @@ import { beforeEach(installStructuredHostStub) afterEach(clearStructuredHostStub) +// A turn the host watched finish that the provider nonetheless failed: the arm +// and the verdict disagree on purpose, which is the shape this surface has to +// carry in both directions. const TURN = { turnId: 'turn-1', state: 'completed' as const, + outcome: 'failure' as const, userItemId: 'user-1', startedAt: 10, completedAt: 42, @@ -132,6 +136,12 @@ describe('turn item projection', () => { it('publishes the status form with the lifecycle intact to a legacy reader', () => { const projected = projectTurnItemHistory(history, STRUCTURED_CLIENT) expect(projected.page.items).toEqual([USER_ITEM, LEGACY_STATUS_ITEM]) + // The downgrade is the only carrier an old client gets, so the verdict has to + // ride inside `turnLifecycle` rather than being dropped with the item kind. + expect(projected.page.items[1]?.body).toMatchObject({ + kind: 'status', + turnLifecycle: { state: 'completed', outcome: 'failure' } + }) // Untouched rows keep their identity; the journal's own body is never mutated. expect(projected.page.items[0]).toBe(USER_ITEM) expect(TURN_ITEM.body.kind).toBe('turn') 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/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/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) + } + ) +}) 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 { 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/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() + } + }) +}) 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 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/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() } diff --git a/src/mobile-web/index.html b/src/mobile-web/index.html new file mode 100644 index 00000000000..9e91c0e3e0b --- /dev/null +++ b/src/mobile-web/index.html @@ -0,0 +1,17 @@ + + + + + + Orca mobile bundle + + + +
+ +

Orca mobile bundle

+
+
+ + + 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 00000000000..274fd7bf782 Binary files /dev/null and b/src/mobile-web/src/orca-mark.png differ diff --git a/src/preload/api/agent-account-api.ts b/src/preload/api/agent-account-api.ts index ff98244299d..f041e41f8b7 100644 --- a/src/preload/api/agent-account-api.ts +++ b/src/preload/api/agent-account-api.ts @@ -11,6 +11,10 @@ export type CodexAccountsApi = { runtime?: 'host' | 'wsl' wslDistro?: string | null }) => Promise + cancelPendingLogin: () => Promise + /** Sign-in link of the login waiting on a browser, or null when none is. */ + getPendingLoginUrl: () => Promise + onPendingLoginUrlChanged: (callback: (url: string | null) => void) => () => void reauthenticate: (args: { accountId: string /** Local-only: activate the re-authed account when its runtime lane had no selection. */ diff --git a/src/preload/api/codex-accounts-bridge.ts b/src/preload/api/codex-accounts-bridge.ts index d085de45855..d5ca98cdf76 100644 --- a/src/preload/api/codex-accounts-bridge.ts +++ b/src/preload/api/codex-accounts-bridge.ts @@ -1,10 +1,20 @@ import { ipcRenderer } from 'electron' +import { CODEX_PENDING_LOGIN_URL_CHANGED_CHANNEL } from '../../shared/codex-auth-errors' import type { PreloadApi } from '../api-types' export const codexAccountsApi = { list: () => ipcRenderer.invoke('codexAccounts:list'), add: (args?: { runtime?: 'host' | 'wsl'; wslDistro?: string | null }) => ipcRenderer.invoke('codexAccounts:add', args), + cancelPendingLogin: (): Promise => + ipcRenderer.invoke('codexAccounts:cancelPendingLogin'), + getPendingLoginUrl: (): Promise => + ipcRenderer.invoke('codexAccounts:pendingLoginUrl'), + onPendingLoginUrlChanged: (callback: (url: string | null) => void): (() => void) => { + const listener = (_event: Electron.IpcRendererEvent, url: string | null): void => callback(url) + ipcRenderer.on(CODEX_PENDING_LOGIN_URL_CHANGED_CHANNEL, listener) + return () => ipcRenderer.removeListener(CODEX_PENDING_LOGIN_URL_CHANGED_CHANNEL, listener) + }, reauthenticate: (args: { accountId: string; activateIfSelectionWasEmpty?: boolean }) => ipcRenderer.invoke('codexAccounts:reauthenticate', args), remove: (args: { accountId: string }) => ipcRenderer.invoke('codexAccounts:remove', args), 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/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/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/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/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 }) 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/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-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() + } + }) +}) 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/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/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) + } } }, []) diff --git a/src/renderer/src/components/activity/activity-clear-completed-hmr.test.ts b/src/renderer/src/components/activity/activity-clear-completed-hmr.test.ts new file mode 100644 index 00000000000..b1698551c5d --- /dev/null +++ b/src/renderer/src/components/activity/activity-clear-completed-hmr.test.ts @@ -0,0 +1,177 @@ +// @vitest-environment happy-dom +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import type { RetainedAgentEntry } from '@/store/slices/agent-status' +import type { ActivityEvent, AgentPaneThread } from './activity-thread-types' +import { makeTab, makeWorktree } from './ActivityPrototypePage-test-fixtures' + +const mockStore = vi.hoisted(() => { + const activityClearedAtByPaneKey: Record = {} + const agentStatusByPaneKey: Record = {} + const retainedAgentsByPaneKey: Record = {} + const retentionSuppressedPaneKeys: Record = {} + const state = { + activityClearedAtByPaneKey, + agentStatusByPaneKey, + retainedAgentsByPaneKey, + retentionSuppressedPaneKeys, + applyActivityClearedAt: vi.fn((patch: Record) => { + const next = { ...state.activityClearedAtByPaneKey } + for (const [key, value] of Object.entries(patch)) { + if (value === null) { + delete next[key] + } else { + next[key] = value + } + } + state.activityClearedAtByPaneKey = next + }), + dismissRetainedAgents: vi.fn((paneKeys: readonly string[]) => { + const next = { ...state.retainedAgentsByPaneKey } + for (const key of paneKeys) { + if (state.agentStatusByPaneKey[key]) { + state.retentionSuppressedPaneKeys[key] = true + } + delete next[key] + } + state.retainedAgentsByPaneKey = next + }), + clearRetentionSuppressedPaneKeys: vi.fn((paneKeys: string[]) => { + for (const key of paneKeys) { + delete state.retentionSuppressedPaneKeys[key] + } + }), + retainAgents: vi.fn((entries: RetainedAgentEntry[]) => { + const next = { ...state.retainedAgentsByPaneKey } + for (const retained of entries) { + next[retained.entry.paneKey] = retained + } + state.retainedAgentsByPaneKey = next + }) + } + return state +}) + +const toastSpy = vi.hoisted(() => vi.fn()) + +vi.mock('@/store', () => ({ + useAppStore: { getState: () => mockStore } +})) +vi.mock('sonner', () => ({ toast: toastSpy })) + +import { + CLEAR_COMPLETED_EVICTION_FALLBACK_MS, + disposePendingClearCompletedEvictionListener, + clearCompletedActivity, + flushPendingClearCompletedEvictions +} from './activity-clear-completed' + +function makeThread(paneKey: string, overrides: Partial = {}): AgentPaneThread { + return { + paneKey, + tab: makeTab(), + worktree: makeWorktree(), + repo: null, + currentAgentState: null, + currentAgentEntry: null, + latestEvent: null, + latestTimestamp: 5_000, + agentType: 'claude', + unread: false, + paneTitle: `Agent ${paneKey}`, + responsePreview: '', + events: [], + ...overrides + } +} + +function doneEvent(interrupted: boolean): ActivityEvent { + return { + id: 'evt', + state: 'done', + timestamp: 5_000, + worktree: makeWorktree(), + repo: null, + entry: { ...makeRetained('t-done:1').entry, interrupted }, + tab: makeTab(), + agentType: 'claude', + agentAlive: false, + unread: false + } +} + +const doneThread = makeThread('t-done:1', { latestEvent: doneEvent(false) }) + +function makeRetained(paneKey: string): RetainedAgentEntry { + return { + entry: { + state: 'done', + prompt: 'retained run', + updatedAt: 5_000, + stateStartedAt: 5_000, + paneKey, + stateHistory: [], + agentType: 'claude' + }, + worktreeId: 'wt-1', + tab: makeTab(), + agentType: 'claude', + startedAt: 5_000 + } +} + +const drop = vi.fn() +const domWindow = window +beforeEach(() => { + vi.useFakeTimers() + Object.assign(domWindow, { api: { agentStatus: { dropPersistedBatch: drop } } }) + mockStore.retainedAgentsByPaneKey = { 't-done:1': makeRetained('t-done:1') } + mockStore.activityClearedAtByPaneKey = {} + mockStore.retentionSuppressedPaneKeys = {} +}) +afterEach(() => { + flushPendingClearCompletedEvictions() + vi.useRealTimers() + vi.clearAllMocks() +}) + +it('preserves the pending pagehide flush through HMR and removes the listener when drained', () => { + const remove = vi.spyOn(domWindow, 'removeEventListener') + try { + clearCompletedActivity([doneThread]) + disposePendingClearCompletedEvictionListener() + expect(remove).not.toHaveBeenCalledWith('pagehide', flushPendingClearCompletedEvictions) + domWindow.dispatchEvent(new Event('pagehide')) + expect(drop).toHaveBeenCalledOnce() + expect(remove).toHaveBeenCalledWith('pagehide', flushPendingClearCompletedEvictions) + } finally { + remove.mockRestore() + } +}) + +it('preserves Undo after HMR without dropping the restored persisted row', () => { + clearCompletedActivity([doneThread]) + disposePendingClearCompletedEvictionListener() + const options = toastSpy.mock.calls.at(-1)?.[1] + options.action.onClick() + domWindow.dispatchEvent(new Event('pagehide')) + vi.advanceTimersByTime(CLEAR_COMPLETED_EVICTION_FALLBACK_MS) + expect(drop).not.toHaveBeenCalled() + expect(mockStore.retainedAgentsByPaneKey['t-done:1']).toBeDefined() +}) + +it('releases the retired listener when its fallback settles and supports a late stale handler', () => { + const remove = vi.spyOn(domWindow, 'removeEventListener') + try { + clearCompletedActivity([doneThread]) + disposePendingClearCompletedEvictionListener() + vi.advanceTimersByTime(CLEAR_COMPLETED_EVICTION_FALLBACK_MS) + expect(drop).toHaveBeenCalledOnce() + expect(remove).toHaveBeenCalledWith('pagehide', flushPendingClearCompletedEvictions) + mockStore.retainedAgentsByPaneKey = { 't-done:1': makeRetained('t-done:1') } + clearCompletedActivity([doneThread]) + domWindow.dispatchEvent(new Event('pagehide')) + expect(drop).toHaveBeenCalledTimes(2) + } finally { + remove.mockRestore() + } +}) diff --git a/src/renderer/src/components/activity/activity-clear-completed.ts b/src/renderer/src/components/activity/activity-clear-completed.ts index 3c6e743a8bb..2bb39fdff43 100644 --- a/src/renderer/src/components/activity/activity-clear-completed.ts +++ b/src/renderer/src/components/activity/activity-clear-completed.ts @@ -68,16 +68,34 @@ export function planClearCompletedActivity( // Deferred evictions whose undo toast is still open; flushed on pagehide because the toast's // close callbacks never fire on quit/reload, which would let cleared rows replay next launch. const pendingDiskEvictions = new Set<() => void>() +let evictionListenerRetired = false export function flushPendingClearCompletedEvictions(): void { // Set iteration tolerates the self-delete each evict() performs. for (const evict of pendingDiskEvictions) { evict() } } +export function disposePendingClearCompletedEvictionListener(): void { + evictionListenerRetired = true + releaseRetiredEvictionListener() +} + +function releaseRetiredEvictionListener(): void { + if (evictionListenerRetired && pendingDiskEvictions.size === 0 && typeof window !== 'undefined') { + window.removeEventListener('pagehide', flushPendingClearCompletedEvictions) + } +} + if (typeof window !== 'undefined') { window.addEventListener('pagehide', flushPendingClearCompletedEvictions) } +if (import.meta !== undefined && import.meta.hot) { + // Vite can replace this module without a full renderer reload. Remove the + // pagehide hook so dev sessions do not retain stale eviction closures. + import.meta.hot.dispose(disposePendingClearCompletedEvictionListener) +} + // Why a fallback: sonner only fires onDismiss/onAutoClose for the toast's own close paths; a // `toast.dismiss()` from another caller leaves the eviction pending until pagehide. export const CLEAR_COMPLETED_EVICTION_FALLBACK_MS = 60_000 @@ -143,6 +161,7 @@ export function clearCompletedActivity(threads: readonly AgentPaneThread[]): boo let fallbackTimer: ReturnType | null = null const dropRetainedFromDiskCache = (): void => { pendingDiskEvictions.delete(dropRetainedFromDiskCache) + releaseRetiredEvictionListener() if (fallbackTimer !== null) { clearTimeout(fallbackTimer) fallbackTimer = null @@ -154,6 +173,9 @@ export function clearCompletedActivity(threads: readonly AgentPaneThread[]): boo evictPersistedStatuses(plan.cacheIdentities) } pendingDiskEvictions.add(dropRetainedFromDiskCache) + if (evictionListenerRetired && typeof window !== 'undefined') { + window.addEventListener('pagehide', flushPendingClearCompletedEvictions) + } fallbackTimer = setTimeout(dropRetainedFromDiskCache, CLEAR_COMPLETED_EVICTION_FALLBACK_MS) toast( plan.clearedThreadCount === 1 @@ -169,6 +191,7 @@ export function clearCompletedActivity(threads: readonly AgentPaneThread[]): boo onClick: () => { undone = true pendingDiskEvictions.delete(dropRetainedFromDiskCache) + releaseRetiredEvictionListener() if (fallbackTimer !== null) { clearTimeout(fallbackTimer) fallbackTimer = null 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/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() diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx index 5b6284ae648..1c176502613 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx @@ -60,6 +60,20 @@ type ContextualTourOverlaySurfaceProps = { onOverlayKeyDownCapture: (event: KeyboardEvent) => void } +function disposeContextualTourGlobalKeyGuard(): void { + if (typeof window === 'undefined') { + return + } + const guardedWindow = window as Window & { + __orcaContextualTourGlobalKeyGuardInstalled?: boolean + } + if (!guardedWindow.__orcaContextualTourGlobalKeyGuardInstalled) { + return + } + window.removeEventListener('keydown', handleContextualTourGlobalKeyDown, true) + delete guardedWindow.__orcaContextualTourGlobalKeyGuardInstalled +} + if (typeof window !== 'undefined') { const guardedWindow = window as Window & { __orcaContextualTourGlobalKeyGuardInstalled?: boolean @@ -70,6 +84,12 @@ if (typeof window !== 'undefined') { } } +if (import.meta !== undefined && import.meta.hot) { + // Vite can replace this module without a full renderer reload. Remove the + // global key guard so dev sessions do not retain stale module closures. + import.meta.hot.dispose(disposeContextualTourGlobalKeyGuard) +} + const PANEL_BASE_CLASSES = 'orca-contextual-tour-panel rounded-lg border border-border text-popover-foreground backdrop-blur-[2px]' diff --git a/src/renderer/src/components/editor/EditorConflictReviewSurface.tsx b/src/renderer/src/components/editor/EditorConflictReviewSurface.tsx index 85c53cfb41f..fbff9caf22e 100644 --- a/src/renderer/src/components/editor/EditorConflictReviewSurface.tsx +++ b/src/renderer/src/components/editor/EditorConflictReviewSurface.tsx @@ -131,6 +131,7 @@ export function EditorConflictReviewSurface({
reloadContent(contentFile)} />
diff --git a/src/renderer/src/components/editor/EditorContent.tsx b/src/renderer/src/components/editor/EditorContent.tsx index 9fab1cd5665..86527b25f29 100644 --- a/src/renderer/src/components/editor/EditorContent.tsx +++ b/src/renderer/src/components/editor/EditorContent.tsx @@ -188,6 +188,7 @@ export function EditorContent({ return ( reloadContent(activeFile)} /> ) diff --git a/src/renderer/src/components/editor/EditorEditFileSurface.tsx b/src/renderer/src/components/editor/EditorEditFileSurface.tsx index 06a7bb3f909..a9e3f8ff856 100644 --- a/src/renderer/src/components/editor/EditorEditFileSurface.tsx +++ b/src/renderer/src/components/editor/EditorEditFileSurface.tsx @@ -101,6 +101,7 @@ export function EditorEditFileSurface({ return ( reloadContent(activeFile)} /> ) diff --git a/src/renderer/src/components/editor/EditorFileLoadErrorView.test.tsx b/src/renderer/src/components/editor/EditorFileLoadErrorView.test.tsx new file mode 100644 index 00000000000..2d0d08fdfbe --- /dev/null +++ b/src/renderer/src/components/editor/EditorFileLoadErrorView.test.tsx @@ -0,0 +1,40 @@ +// @vitest-environment happy-dom +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { EditorFileLoadErrorView } from './EditorFileLoadErrorView' +import { + WORKTREE_HOST_UNRESOLVED_CODE, + WORKTREE_HOST_UNRESOLVED_ERROR +} from './editor-panel-content-types' + +describe('EditorFileLoadErrorView', () => { + afterEach(cleanup) + + it('offers Retry as its only action', () => { + // Why: closing must stay with the tab strip, whose path carries the pin, shared- + // reference, and unsaved-changes checks; a second close control here would not. + const onRetry = vi.fn() + + render() + + screen.getByText('selector_not_found') + expect(screen.getAllByRole('button')).toHaveLength(1) + fireEvent.click(screen.getByRole('button', { name: 'Retry' })) + expect(onRetry).toHaveBeenCalledOnce() + }) + + it('localizes the host-unresolved state by its sentinel code, not by the stored text', () => { + // Why: the stored `loadError` is an English fallback; the code is what selects the + // localized copy, so a translated catalog cannot desynchronize from the comparison. + render( + + ) + + screen.getByText(WORKTREE_HOST_UNRESOLVED_ERROR) + expect(screen.queryByText('stored fallback text')).toBeNull() + }) +}) diff --git a/src/renderer/src/components/editor/EditorFileLoadErrorView.tsx b/src/renderer/src/components/editor/EditorFileLoadErrorView.tsx index 41964fbd444..361e869d4ad 100644 --- a/src/renderer/src/components/editor/EditorFileLoadErrorView.tsx +++ b/src/renderer/src/components/editor/EditorFileLoadErrorView.tsx @@ -1,12 +1,32 @@ import { AlertCircle, RefreshCw } from 'lucide-react' import { Button } from '@/components/ui/button' import { translate } from '@/i18n/i18n' +import { WORKTREE_HOST_UNRESOLVED_CODE } from './editor-panel-content-types' +// Why: `loadError` is stored as English so logs and non-view consumers stay readable; the +// user-facing copy is keyed by the machine sentinel, never by the text, so localization +// cannot break the terminal-state comparison upstream (#21041). +function localizeFileLoadError(message: string, code: string | undefined): string { + if (code === WORKTREE_HOST_UNRESOLVED_CODE) { + return translate( + 'editor.fileLoad.hostUnresolved', + "The host couldn't find this file's workspace. It may have been removed, or the host may not know about it yet. Retry, or close this tab from the tab strip." + ) + } + return message +} + +// Why no Close action here: this view renders for real tabs and for synthesized inline +// conflict rows alike, and only the tab strip's own close path carries the pin, shared- +// reference, and unsaved-changes semantics. The copy points the user at that path instead +// of adding a second one that would have to reimplement it (#21041). export function EditorFileLoadErrorView({ message, + code, onRetry }: { message: string + code?: string onRetry: () => void }): React.JSX.Element { return ( @@ -17,7 +37,7 @@ export function EditorFileLoadErrorView({
{translate('auto.components.editor.EditorContent.39f018b052', 'Unable to load file')}
-
{message}
+
{localizeFileLoadError(message, code)}
+ + @@ -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 ( 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 (
{remoteAccountScopeNotice} + {/* Why not in a remote scope: the link belongs to a login running on + this desktop, which has nothing to do with the server named above. */} + {isRemoteAccountScope ? null : }
- - -
-
{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/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/ipc-pty-connect.ts b/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts index 3023236de0e..52b983c51fd 100644 --- a/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts +++ b/src/renderer/src/components/terminal-pane/ipc-pty-connect.ts @@ -27,6 +27,7 @@ type IpcPtyConnectContext = { /** True only for the one buffered exit consumed by this connect attempt. */ isExpectedExitCurrent: () => boolean ownsPtyId: (id: string) => boolean + handleExplicitlyClosedConnect?: (id: string) => boolean bind: (id: string) => void isCurrent: (id: string) => boolean setCallbacks: (callbacks: PtyConnectOptions['callbacks']) => void @@ -89,6 +90,9 @@ export async function connectIpcPty( const priorIncarnationFence = currentPreHandlerPtySequence() const spawnResult = await spawnIpcPty(transportOptions, options, admittedSessionId) const retireFreshSpawn = async (): Promise => { + if (context.handleExplicitlyClosedConnect?.(spawnResult.id)) { + return + } // A newer generation may already own a recycled id; an id-only kill would retire its PTY. if ( !spawnResult.isReattach && diff --git a/src/renderer/src/components/terminal-pane/pane-title-update-global-scan-budget.test.tsx b/src/renderer/src/components/terminal-pane/pane-title-update-global-scan-budget.test.tsx new file mode 100644 index 00000000000..f511814cb2b --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pane-title-update-global-scan-budget.test.tsx @@ -0,0 +1,419 @@ +// @vitest-environment happy-dom +/** + * Deterministic, count-based reproduction for STA-7552 (under STA-7551). + * + * Zustand notifies every subscriber synchronously on every `set`, and each + * subscriber re-runs its selector. A single pane title update therefore pays + * for every mounted selector that rescans a global collection to conclude + * nothing changed for it. `useShallow` suppresses the *re-render*, never the + * selector body, so it does not help here. + * + * Scale is Jinjing's live 1.4.203-hourly capture: ~870 workspaces, ~1,400 + * terminal tabs, ~857 sleeping-agent records, ~177 agent-status rows, 20 + * mounted panes/cards. + * + * Scale check: this mount opens 5,500 zustand listeners, inside the capture's + * 5,462-7,478. Most come from the sidebar — the worktree list is not + * virtualised, so all 870 rows mount and each opens ~6 subscriptions. + * + * ONE `setRuntimePaneTitle` at that scale, before/after the park-exemption memo: + * + * metric before after + * zustand listeners 5,500 5,500 + * store notifications 1 1 + * listener invocations 5,500 5,500 + * selector runs: worktree activity summary 896 896 + * selector runs: worktree card status inputs 2,688 2,688 + * selector runs: sleeping-record exemption 23 23 + * sidebar rows committed (of 870) 1 1 + * React commits: retained panes 1 1 + * sleeping-agent records read 19,711 0 + * agent-status rows read 0 0 + * workspace tab buckets read 1,394 1,394 + * + * So notification work is O(mounted workspaces) and this fix does not change + * that — one shared store means every subscriber is visited. What changes is + * the cost of each visit: the scan is gone, and 5,499 of the 5,500 invocations + * were already resolving to "nothing changed for me" without re-rendering. + */ +import React, { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useShallow } from 'zustand/react/shallow' +import { useAppStore } from '@/store' +import { selectTabBarAgentProjections } from '@/components/tab-bar/tab-agent-types-by-tab-id' +import { useWorktreeActivityStatus } from '@/components/sidebar/use-worktree-activity-status' +import { WorktreeCardStatusSlot } from '@/components/sidebar/WorktreeCardStatusSlot' +import { TooltipProvider } from '@/components/ui/tooltip' +import { readStoreListenerCount } from '@/store/store-listener-census' +import type { AgentStatusEntry } from '../../../../shared/agent-status-types' +import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import { useTerminalTabColdParking } from './use-terminal-tab-cold-parking' +// Why namespace type imports: vi.mock factories are hoisted, and `typeof import()` +// annotations are banned, so the module shapes come from erased type-only imports. +import type * as SleepingRecordParkExemptionModule from './sleeping-record-park-exemption' +import type * as WorktreeAgentActivitySummaryModule from '@/components/sidebar/worktree-agent-activity-summary' +import type * as WorktreeCardStatusInputsModule from '@/components/sidebar/worktree-card-status-inputs' + +Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true) + +const WORKSPACE_COUNT = 870 +const TERMINAL_TAB_COUNT = 1408 +const SLEEPING_RECORD_COUNT = 857 +const AGENT_STATUS_COUNT = 177 +/** "Live or mounted panes: 20–28" in the capture. */ +const MOUNTED_WORKTREE_COUNT = 20 + +const reads = { sleepingRecords: 0, agentStatusRows: 0, workspaceTabBuckets: 0 } + +/** True per-module selector executions. Cached selectors read no records, so the + * scan counters alone cannot tell "never ran" from "ran and short-circuited". */ +const selectorRuns = vi.hoisted(() => ({ + sleepingRecordParkExemption: 0, + worktreeAgentActivitySummary: 0, + worktreeCardStatusInputs: 0 +})) + +vi.mock('./sleeping-record-park-exemption', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + selectSleepingRecordParkExemptTabIds: ( + ...args: Parameters + ) => { + selectorRuns.sleepingRecordParkExemption += 1 + return actual.selectSleepingRecordParkExemptTabIds(...args) + } + } +}) + +vi.mock('@/components/sidebar/worktree-agent-activity-summary', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + selectWorktreeAgentActivitySummary: ( + ...args: Parameters + ) => { + selectorRuns.worktreeAgentActivitySummary += 1 + return actual.selectWorktreeAgentActivitySummary(...args) + } + } +}) + +vi.mock('@/components/sidebar/worktree-card-status-inputs', async (importOriginal) => { + const actual = await importOriginal() + const count = ( + select: (...args: TArgs) => TResult + ): ((...args: TArgs) => TResult) => { + return (...args: TArgs) => { + selectorRuns.worktreeCardStatusInputs += 1 + return select(...args) + } + } + return { + ...actual, + selectRuntimePaneTitlesForWorktree: count(actual.selectRuntimePaneTitlesForWorktree), + selectLivePtyIdsForWorktree: count(actual.selectLivePtyIdsForWorktree), + selectTerminalLayoutRootsForWorktree: count(actual.selectTerminalLayoutRootsForWorktree) + } +}) + +/** Workspaces whose sidebar-row subtree committed. Why a `Profiler` and not a + * counter in the wrapper: `WorktreeCardStatusSlot` subscribes to the store + * itself, so it can commit without re-executing anything above it. */ +const committedSidebarRows = new Set() +/** React commits of the retained-pane hooks, which live in the probe body. */ +const renders = { retainedPanes: 0 } +/** Store notifications; every live listener is visited on each one. */ +let notifications = 0 + +/** Counts every value read, so a `for…in`/`Object.values` walk is visible without touching production code. */ +function countingRecord( + entries: readonly (readonly [string, T])[], + counter: keyof typeof reads +): Record { + const map: Record = {} + for (const [key, value] of entries) { + Object.defineProperty(map, key, { + enumerable: true, + configurable: true, + get: () => { + reads[counter] += 1 + return value + } + }) + } + return map +} + +const worktreeIds = Array.from( + { length: WORKSPACE_COUNT }, + (_, index) => `repo-1::/repo/wt-${index}` +) +const mountedWorktreeIds = worktreeIds.slice(0, MOUNTED_WORKTREE_COUNT) +/** The pane that receives the title update, on the first mounted workspace. */ +const TARGET_WORKTREE_ID = mountedWorktreeIds[0] +const TARGET_TAB_ID = 'tab-0-0' +const TARGET_PANE_ID = 1 + +function buildTabsByWorktree(): Record { + const tabsByWorktree: Record = {} + let remaining = TERMINAL_TAB_COUNT + for (const [index, worktreeId] of worktreeIds.entries()) { + const count = Math.min(remaining, index < MOUNTED_WORKTREE_COUNT ? 4 : 2) + remaining -= count + tabsByWorktree[worktreeId] = Array.from({ length: count }, (_, tabIndex) => ({ + id: `tab-${index}-${tabIndex}`, + ptyId: `${worktreeId}@@pty-${tabIndex}`, + worktreeId, + title: `tab ${tabIndex}`, + customTitle: null, + color: null, + sortOrder: tabIndex, + createdAt: 0 + })) + if (remaining <= 0) { + break + } + } + return tabsByWorktree +} + +const seededTabsByWorktree = buildTabsByWorktree() +const seededTerminalTabs = Object.values(seededTabsByWorktree).flat() +/** Counting view of the workspace inventory: one hit per bucket actually read, + * so "looked up my own workspace" and "walked all 870" are different numbers. */ +const tabsByWorktree = countingRecord(Object.entries(seededTabsByWorktree), 'workspaceTabBuckets') + +function buildSleepingRecords(): Record { + return countingRecord( + Array.from({ length: SLEEPING_RECORD_COUNT }, (_, index) => { + const worktreeId = worktreeIds[index % WORKSPACE_COUNT] + const tabId = seededTabsByWorktree[worktreeId]?.[0]?.id ?? `tab-${index}-0` + const paneKey = `${tabId}:1` + const record: SleepingAgentSessionRecord = { + paneKey, + tabId, + worktreeId, + agent: 'claude', + providerSession: { key: 'session_id', id: `session-${index}` }, + prompt: 'prompt', + state: 'working', + capturedAt: 1, + updatedAt: 1 + } + return [paneKey, record] as const + }), + 'sleepingRecords' + ) +} + +function buildAgentStatuses(): Record { + const now = Date.now() + return countingRecord( + Array.from({ length: AGENT_STATUS_COUNT }, (_, index) => { + const tabId = seededTerminalTabs[index % seededTerminalTabs.length].id + const paneKey = `${tabId}:1` + const entry: AgentStatusEntry = { + paneKey, + state: 'working', + prompt: 'prompt', + updatedAt: now, + stateStartedAt: now, + stateHistory: [], + agentType: 'claude' + } + return [paneKey, entry] as const + }), + 'agentStatusRows' + ) +} + +const originalState = useAppStore.getState() +let container: HTMLDivElement | null = null +let root: Root | null = null + +const EMPTY_ASSIGNMENTS = new Map() +const noop = (): void => {} +function recordSidebarRowCommit(worktreeId: string): void { + committedSidebarRows.add(worktreeId) +} + +/** The real sidebar row. `useWorktreeActivityStatus` opens ~6 store + * subscriptions per row, which is where the capture's thousands of listeners + * come from — the sidebar is not virtualised, so every workspace is mounted. */ +function SidebarRowProbe({ worktreeId }: { worktreeId: string }): React.JSX.Element { + return ( + + + + ) +} + +/** The three consumers STA-7552 names, mounted per retained workspace. */ +function MountedWorkspaceProbe({ worktreeId }: { worktreeId: string }): null { + renders.retainedPanes += 1 + useTerminalTabColdParking({ + worktreeId, + terminalTabs: seededTabsByWorktree[worktreeId] ?? [], + assignments: EMPTY_ASSIGNMENTS, + isWorktreeActive: worktreeId === TARGET_WORKTREE_ID, + activeTerminalTabId: null, + coldParkTerminalPanes: false, + shouldMeasureHiddenWorktree: false, + activityTerminalPortals: [], + activationDeferredMountTabIds: null + }) + useWorktreeActivityStatus(worktreeId) + useAppStore(useShallow(selectTabBarAgentProjections)) + return null +} + +function mountAtCaptureScale(): void { + useAppStore.setState({ + tabsByWorktree, + sleepingAgentSessionsByPaneKey: buildSleepingRecords(), + agentStatusByPaneKey: buildAgentStatuses(), + agentStatusEpoch: 1, + activeWorktreeId: TARGET_WORKTREE_ID, + runtimePaneTitlesByTabId: { [TARGET_TAB_ID]: { [TARGET_PANE_ID]: 'initial title' } } + }) + + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => + root?.render( + + {mountedWorktreeIds.map((worktreeId) => ( + + ))} + {worktreeIds.map((worktreeId) => ( + + ))} + + ) + ) +} + +function liveListenerCount(): number { + const count = readStoreListenerCount() + if (count === null) { + throw new Error('store listener census unavailable') + } + return count +} + +function resetCounters(): void { + reads.sleepingRecords = 0 + reads.agentStatusRows = 0 + reads.workspaceTabBuckets = 0 + committedSidebarRows.clear() + renders.retainedPanes = 0 + notifications = 0 + selectorRuns.sleepingRecordParkExemption = 0 + selectorRuns.worktreeAgentActivitySummary = 0 + selectorRuns.worktreeCardStatusInputs = 0 +} + +function applyOnePaneTitleUpdate(title: string): void { + act(() => { + useAppStore.getState().setRuntimePaneTitle(TARGET_TAB_ID, TARGET_PANE_ID, title) + }) +} + +let stopNotificationProbe: (() => void) | null = null + +beforeEach(() => { + resetCounters() +}) + +afterEach(() => { + stopNotificationProbe?.() + stopNotificationProbe = null + if (root) { + act(() => root?.unmount()) + } + root = null + container?.remove() + container = null + useAppStore.setState(originalState, true) +}) + +describe('one pane title update at live-capture scale', () => { + it('still rescans when the sleeping-record inventory itself changes', () => { + mountAtCaptureScale() + reads.sleepingRecords = 0 + + act(() => { + useAppStore.setState({ sleepingAgentSessionsByPaneKey: buildSleepingRecords() }) + }) + + // Why: correctness floor — a real inventory change must still be observed. + expect(reads.sleepingRecords).toBeGreaterThanOrEqual(SLEEPING_RECORD_COUNT) + }) +}) + +describe('one pane title update: fanout at live-capture scale', () => { + it('reports the four counts the ticket asks for', () => { + mountAtCaptureScale() + const listeners = liveListenerCount() + resetCounters() + stopNotificationProbe = useAppStore.subscribe(() => { + notifications += 1 + }) + + applyOnePaneTitleUpdate('next title') + + // The capture saw 5,462–7,478 listeners; this mount must be the same order, + // and zustand visits every one of them on each notification. + expect(listeners).toBeGreaterThan(5_000) + expect(listeners).toBeLessThan(8_000) + expect(notifications).toBe(1) + + // Every mounted subscriber's selector still runs. That is unchanged by this + // fix and is inherent to one shared store: notification work stays + // O(mounted workspaces), ~3,630 instrumented selector executions. + expect(selectorRuns.worktreeAgentActivitySummary).toBeGreaterThanOrEqual(WORKSPACE_COUNT) + expect(selectorRuns.worktreeCardStatusInputs).toBeGreaterThanOrEqual(WORKSPACE_COUNT * 3) + expect(selectorRuns.sleepingRecordParkExemption).toBeGreaterThanOrEqual(MOUNTED_WORKTREE_COUNT) + + // …but every one of those executions is now an identity check. These three + // counters sit on the state maps themselves, so they catch a walk by ANY of + // the 5,500 subscribers, not only the three instrumented modules. + expect(reads.sleepingRecords).toBe(0) + expect(reads.agentStatusRows).toBe(0) + // One bucket lookup per workspace consumer is a keyed read; a full-inventory + // walk would be that many times 870. + expect(reads.workspaceTabBuckets).toBeLessThan(WORKSPACE_COUNT * 2) + + // Only the workspace that owns the changed pane commits — the other 869 + // sidebar rows hold their identities and bail out. + expect([...committedSidebarRows]).toEqual([TARGET_WORKTREE_ID]) + expect(renders.retainedPanes).toBeLessThanOrEqual(1) + }) + + it('stays flat across repeated updates', () => { + mountAtCaptureScale() + resetCounters() + + applyOnePaneTitleUpdate('title a') + applyOnePaneTitleUpdate('title b') + applyOnePaneTitleUpdate('title c') + + // Why repeat: one update could be served by a memo warmed at mount; three + // prove the cost is independent of how many records the profile stores. + expect(reads.sleepingRecords).toBe(0) + expect(reads.agentStatusRows).toBe(0) + expect([...committedSidebarRows]).toEqual([TARGET_WORKTREE_ID]) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts b/src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts new file mode 100644 index 00000000000..03f8887564d --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts @@ -0,0 +1,192 @@ +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { preparePendingSplitClose } from './pending-split-close-test-fixture' +import { flushPtySideEffects } from './pty-transport-test-harness' +import type { PtyRunningWorkProbe } from '../terminal/pty-running-work-probe' + +beforeEach(() => vi.clearAllMocks()) +afterEach(() => vi.useRealTimers()) + +async function prepare(remote = false, requestedPtyId?: string) { + const p = await preparePendingSplitClose(requestedPtyId) + Object.assign(p.state, { settings: { skipCloseTerminalWithRunningProcessConfirm: false } }) + const { probePtyRunningWork } = await import('../terminal/pty-running-work-probe') + const { useTerminalPaneCloseActions } = await import('./use-terminal-pane-close-actions') + // Supply the dialog state that the UI renders after setPendingCloseConfirmation. + // oxlint-disable-next-line react-hooks/rules-of-hooks -- React registration is mocked; exercise public close callbacks without mounting UI. + const actions = useTerminalPaneCloseActions({ + ...p.controller, + pendingCloseConfirmation: { paneId: 1, copyKind: 'command' } + }) + const reply = Promise.withResolvers() + vi.mocked(probePtyRunningWork).mockReturnValueOnce(reply.promise) + const verdict = (value: PtyRunningWorkProbe['verdict']) => + reply.resolve([{ ptyId: 'captured', verdict: value, timedOut: false, remote }]) + const closed = () => vi.mocked(window.api.pty.kill).mock.calls.length > 0 + const settle = async () => { + p.spawn.resolve({ id: requestedPtyId ?? 'pty-restored', isReattach: true }) + await p.connecting + await flushPtySideEffects() + } + return { ...p, actions, probePtyRunningWork, verdict, reply, closed, settle } +} + +it.each([false])('requires confirmation for pending live work, paired=%s', async (remote) => { + const p = await prepare(remote) + p.actions.handleRequestClosePane(1) + expect(p.probePtyRunningWork).toHaveBeenCalledWith( + expect.any(Object), + [remote ? 'remote:env-1@@term_original' : 'pty-restored'], + expect.any(Object) + ) + expect(p.closed()).toBe(false) + p.verdict('live') + await flushPtySideEffects() + expect(p.controller.setPendingCloseConfirmation).toHaveBeenCalledWith( + expect.objectContaining({ paneId: 1 }) + ) + expect(p.closed()).toBe(false) + p.actions.handleConfirmClose(false) + await p.settle() + expect(p.closed()).toBe(true) +}) + +it.each([false])('Cancel preserves pending work, paired=%s', async (remote) => { + const p = await prepare(remote) + p.actions.handleRequestClosePane(1) + p.verdict('live') + await flushPtySideEffects() + p.actions.handleCancelClose() + p.actions.handleConfirmClose(false) + await p.settle() + expect(p.closed()).toBe(false) +}) + +it.each(['unverifiable', 'rejected', 'timeout'] as const)( + 'requires confirmation when a pending owner probe is %s', + async (mode) => { + vi.useFakeTimers() + const p = await prepare() + p.actions.handleRequestClosePane(1) + if (mode === 'rejected') { + p.reply.reject(new Error('owner unavailable')) + } else if (mode === 'unverifiable') { + p.verdict(mode) + } else { + await vi.advanceTimersByTimeAsync(101) + } + await Promise.resolve() + await Promise.resolve() + expect(p.controller.setPendingCloseConfirmation).toHaveBeenCalled() + expect(p.closed()).toBe(false) + p.actions.handleCancelClose() + p.verdict('exited') + vi.useRealTimers() + await p.settle() + expect(p.closed()).toBe(false) + } +) + +it('uses the direct SSH identity in the running-work probe', async () => { + const id = 'ssh:ssh-1@@pty-restored' + const p = await prepare(false, id) + p.state.worktreesByRepo = { repo: [{ id: 'workspace', repoId: 'repo', hostId: 'ssh:ssh-1' }] } + p.actions.handleRequestClosePane(1) + p.verdict('live') + await flushPtySideEffects() + expect(p.probePtyRunningWork).toHaveBeenCalledWith(expect.any(Object), [id], expect.any(Object)) + expect(p.closed()).toBe(false) + p.actions.handleConfirmClose(false) + await p.settle() + expect(window.api.pty.kill).toHaveBeenCalledWith(id) +}) + +it.each(['rejected', 'timeout'] as const)( + 'honors the skip-confirmation setting when a pending probe is %s', + async (mode) => { + vi.useFakeTimers() + const p = await prepare() + Object.assign(p.state, { settings: { skipCloseTerminalWithRunningProcessConfirm: true } }) + p.actions.handleRequestClosePane(1) + if (mode === 'rejected') { + p.reply.reject(new Error('owner unavailable')) + } else { + await vi.advanceTimersByTimeAsync(101) + } + await Promise.resolve() + await Promise.resolve() + expect(p.controller.setPendingCloseConfirmation).not.toHaveBeenCalled() + expect(p.closed()).toBe(true) + p.verdict('exited') + vi.useRealTimers() + await p.settle() + } +) + +it.each(['exited', 'skip-setting'] as const)('keeps allowed pending close for %s', async (mode) => { + const p = await prepare() + if (mode === 'skip-setting') { + Object.assign(p.state, { settings: { skipCloseTerminalWithRunningProcessConfirm: true } }) + } + p.actions.handleRequestClosePane(1) + p.verdict(mode === 'exited' ? 'exited' : 'live') + await flushPtySideEffects() + expect(p.controller.setPendingCloseConfirmation).not.toHaveBeenCalled() + await p.settle() + expect(p.closed()).toBe(true) +}) + +it.each(['probe', 'dialog'] as const)( + 'accepts the same pending attach finishing during %s', + async (phase) => { + const p = await prepare() + p.actions.handleRequestClosePane(1) + if (phase === 'dialog') { + p.verdict('live') + await flushPtySideEffects() + } + p.spawn.resolve({ id: 'pty-restored', isReattach: true }) + await p.connecting + p.verdict('live') + await flushPtySideEffects() + expect(p.closed()).toBe(false) + p.actions.handleConfirmClose(false) + await flushPtySideEffects() + expect(p.closed()).toBe(true) + } +) + +it.each(['tab', 'generation', 'leaf', 'transport', 'manager', 'whole-tab', 'binding'] as const)( + 'does not apply a pending confirmation to a replacement %s', + async (replacement) => { + const p = await prepare() + p.actions.handleRequestClosePane(1) + p.verdict('live') + await flushPtySideEffects() + if (replacement === 'tab') { + p.state.tabsByWorktree.workspace[0].createdAt += 1 + } + if (replacement === 'generation') { + p.state.tabsByWorktree.workspace[0].generation = 1 + } + if (replacement === 'leaf') { + p.state.terminalLayoutsByTabId[p.tabId].ptyIdsByLeafId = {} + } + if (replacement === 'binding') { + p.state.terminalLayoutsByTabId[p.tabId].ptyIdsByLeafId = { [p.leafId]: 'successor' } + } + if (replacement === 'transport') { + p.transports.delete(1) + } + if (replacement === 'manager') { + p.controller.managerRef.current = null + } + if (replacement === 'whole-tab') { + vi.spyOn(p.controller.managerRef.current!, 'getPanes').mockReturnValue([]) + } + p.actions.handleConfirmClose(false) + await p.settle() + expect(p.closed()).toBe(false) + expect(p.controller.onCloseTab).not.toHaveBeenCalled() + p.transport.detach?.({ preserveExitObserver: false }) + } +) diff --git a/src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts b/src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts new file mode 100644 index 00000000000..66bfa3f9307 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts @@ -0,0 +1,207 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the sibling + pending-split-close specs, not shipped code, and it falls outside the *.test / *.spec / tests + glob set this rule is already switched off for. The calls have to live in one shared module: + `vi.mock` is registered per importing spec, so the alternative is copying all twelve into every + spec, where they would drift apart. */ +import { afterEach, beforeEach, vi } from 'vitest' +import type { AppState } from '@/store/types' +import type { TerminalTabRetirementState } from '@/store/slices/terminal-tab-retirement' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { PaneManager } from '@/lib/pane-manager/pane-manager' +import type { PtyConnectResult, PtyTransport } from './pty-transport-types' +import type { TerminalPaneBindingController } from './use-terminal-pane-layout-bindings' +import { installIpcPtyWindow, restorePtySpecWindow } from './pty-transport-test-harness' + +const store = vi.hoisted((): { current: AppState | null } => ({ current: null })) +vi.mock('../../store', () => ({ useAppStore: { getState: () => store.current } })) +vi.mock('react', () => ({ + useCallback: (fn: unknown) => fn, + useImperativeHandle: () => {}, + useRef: (current: unknown) => ({ current }) +})) +vi.mock('../../runtime/web-runtime-session', () => ({ closeWebRuntimeTerminal: vi.fn() })) +vi.mock('../../runtime/sync-runtime-graph', () => ({ scheduleRuntimeGraphSync: vi.fn() })) +vi.mock('../terminal/terminal-close-copy-kind', () => ({ resolveLeafCloseCopyKind: vi.fn() })) +vi.mock('../terminal/running-terminal-close-guard', () => ({ RUNNING_CLOSE_PROBE_TIMEOUT_MS: 100 })) +vi.mock('../terminal/pty-running-work-probe', () => ({ probePtyRunningWork: vi.fn() })) +vi.mock('./terminal-pane-tab-detach', () => ({ + detachTerminalPaneToTab: vi.fn(), + isTerminalTabStripDropTarget: vi.fn(), + resolveTerminalTabStripDropTarget: vi.fn() +})) +vi.mock('./terminal-pane-close-identity', () => ({ + resolveTabTitleAfterPaneClose: vi.fn(), + shouldClearLaunchAgentForClosedPane: () => false +})) +vi.mock('./terminal-pane-lifecycle-primitives', () => ({ reportActiveRendererPtyForPane: vi.fn() })) +vi.mock('./deferred-split-pane-handoff', () => ({ + clearDeferredSplitPaneHandoff: vi.fn(), + discardDeferredSplitPaneHandoffForKey: vi.fn() +})) +vi.mock('./expand-collapse', () => ({ useExpandCollapseActions: () => ({}) })) + +const originalWindow = globalThis.window +beforeEach(() => { + vi.resetModules() + installIpcPtyWindow(originalWindow, {}) +}) +afterEach(() => { + restorePtySpecWindow(originalWindow) + vi.restoreAllMocks() +}) + +export function makeCloseTestTab( + id: string, + ptyId: string | null, + worktreeId = 'workspace' +): TerminalTab { + return { + id, + worktreeId, + ptyId, + title: id, + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 0 + } +} + +export async function preparePendingSplitClose( + requestedPtyId = 'pty-restored', + worktreeId = 'workspace' +) { + const { createIpcPtyTransport } = await import('./pty-transport') + const { useTerminalPaneLayoutBindings } = await import('./use-terminal-pane-layout-bindings') + const { useTerminalPaneCloseActions } = await import('./use-terminal-pane-close-actions') + const { createTerminalPaneClosedHandler } = await import('./terminal-pane-pane-closed') + const { useTerminalPaneLifecycleRefs } = await import('./use-terminal-pane-lifecycle-refs') + const tabId = 'tab-parent' + const leafId = '11111111-1111-4111-8111-111111111111' + const siblingLeafId = '22222222-2222-4222-8222-222222222222' + const state: TerminalTabRetirementState = { + worktreesByRepo: { repo: [{ id: worktreeId, repoId: 'repo', hostId: 'local' }] }, + tabsByWorktree: { [worktreeId]: [makeCloseTestTab(tabId, requestedPtyId, worktreeId)] }, + unifiedTabsByWorktree: {}, + ptyIdsByTabId: { [tabId]: [requestedPtyId] }, + lastKnownRelayPtyIdByTabId: {}, + deferredSshSessionIdsByTabId: {}, + pendingReconnectPtyIdByTabId: {}, + terminalLayoutsByTabId: { + [tabId]: { + root: { + type: 'split', + direction: 'horizontal', + ratio: 0.5, + first: { type: 'leaf', leafId }, + second: { type: 'leaf', leafId: siblingLeafId } + }, + activeLeafId: leafId, + expandedLeafId: null, + ptyIdsByLeafId: { [leafId]: requestedPtyId, [siblingLeafId]: 'pty-sibling' } + } + } + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The real close paths read only this retirement state and the supplied actions. + store.current = Object.assign(state, { + setCacheTimerStartedAt: vi.fn(), + dropAgentStatus: vi.fn(), + retireAgentPaneAuthority: vi.fn(), + suppressPtyExit: vi.fn() + }) as unknown as AppState + const spawn = Promise.withResolvers() + vi.mocked(window.api.pty.spawn).mockReturnValueOnce(spawn.promise) + const transport = createIpcPtyTransport({}) + const connecting = transport.connect({ url: '', sessionId: requestedPtyId, callbacks: {} }) + let onClosed: ReturnType = () => {} + let panes = [ + { id: 1, leafId }, + { id: 2, leafId: siblingLeafId } + ] + const managerFixture = { + getPanes: () => panes, + getLeafId: () => leafId, + getActivePane: () => null, + closePane(id: number) { + panes = panes.filter((pane) => pane.id !== id) + onClosed(id, { leafId, reason: 'close' }) + } + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: These real close methods use only the four manager operations above; no DOM is mounted. + const manager = managerFixture as unknown as PaneManager + const transports = new Map([[1, transport]]) + const partial: Partial = { + tabId, + worktreeId, + managerRef: { current: manager }, + paneTransportsRef: { current: transports }, + panePtyBindingsRef: { current: new Map() }, + paneMode2031Ref: { current: new Map() }, + paneKittyKeyboardModesRef: { current: new Map() }, + paneLastThemeModeRef: { current: new Map() }, + paneCwdRef: { current: new Map() }, + paneFontSizesRef: { current: new Map() }, + replayingPanesRef: { current: new Map() }, + paneTitlesRef: { current: {} }, + expandedPaneIdRef: { current: null }, + expandedStyleSnapshotRef: { current: new Map() }, + containerRef: { current: null }, + pendingPaneSizeRefreshFrameIdsRef: { current: [] }, + ref: { current: null }, + clearSessionRestoredBannerForPane: vi.fn(), + persistLayoutSnapshot: vi.fn(), + setPendingCloseConfirmation: vi.fn(), + setTerminalErrorsByPaneId: vi.fn(), + updateSettings: vi.fn(), + setExpandedPaneId: vi.fn(), + setTabPaneExpanded: vi.fn(), + onCloseTab: vi.fn(), + clearTabPtyId: vi.fn(), + clearRuntimePaneTitle: vi.fn(), + setPaneTitles: vi.fn(), + setRenamingPaneId: vi.fn(), + setPaneCount: vi.fn(), + updateTabTitle: vi.fn(), + setTabLayout: (_tabId, layout) => { + if (layout) { + state.terminalLayoutsByTabId[tabId] = layout + } else { + delete state.terminalLayoutsByTabId[tabId] + } + } + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Every controller field read by the three exercised hooks is provided above or assigned from the real binding hook below. + const controller = partial as TerminalPaneBindingController + // oxlint-disable-next-line react-hooks/rules-of-hooks -- React registration is mocked; exercise the real binding callbacks without mounting UI. + Object.assign(controller, useTerminalPaneLayoutBindings(controller)) + const closeContext = { + deps: { + ...controller, + effectiveMacOptionAsAltRef: { current: 'false' as const }, + consumeSuppressedPtyExit: () => false, + isPtyShutdownPending: () => false, + onShowSessionRestoredBanner: vi.fn() + }, + // oxlint-disable-next-line react-hooks/rules-of-hooks -- The mocked useRef allocates the real per-pane ownership registries. + refs: useTerminalPaneLifecycleRefs(), + deferredSplitHandoffs: new Map() + } + onClosed = createTerminalPaneClosedHandler( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The real close handler reads only deps, refs, and deferredSplitHandoffs from its broader mount context. + closeContext as unknown as Parameters[0] + ) + // oxlint-disable-next-line react-hooks/rules-of-hooks -- React registration is mocked; exercise the real close callbacks without mounting UI. + const actions = useTerminalPaneCloseActions(controller) + return { + transport, + transports, + connecting, + spawn, + controller, + actions, + state, + tabId, + leafId, + siblingLeafId + } +} diff --git a/src/renderer/src/components/terminal-pane/pending-split-close.test.ts b/src/renderer/src/components/terminal-pane/pending-split-close.test.ts new file mode 100644 index 00000000000..5a022ef9c99 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pending-split-close.test.ts @@ -0,0 +1,193 @@ +import { expect, it, vi } from 'vitest' +import { makeCloseTestTab, preparePendingSplitClose } from './pending-split-close-test-fixture' +import { folderWorkspaceKey } from '../../../../shared/workspace-scope' +import { flushPtySideEffects } from './pty-transport-test-harness' + +const replyFor = (kind: 'reattach' | 'cold-restore-new' | 'ordinary-fresh') => ({ + id: 'pty-restored', + ...(kind === 'reattach' ? { isReattach: true } : {}), + ...(kind === 'cold-restore-new' ? { coldRestore: { scrollback: 'saved', cwd: '/tmp' } } : {}) +}) + +it.each(['reattach', 'cold-restore-new', 'ordinary-fresh'] as const)( + 'explicit split close retires a pending %s before and after its reply', + async (kind) => { + const p = await preparePendingSplitClose() + expect(p.transport.getPtyId()).toBeNull() + p.actions.executeClosePane(1) + expect(p.transports.has(1)).toBe(false) + expect(p.state.terminalLayoutsByTabId[p.tabId].ptyIdsByLeafId?.[p.leafId]).toBeUndefined() + expect(window.api.pty.kill).toHaveBeenCalledExactlyOnceWith('pty-restored') + p.spawn.resolve(replyFor(kind)) + await p.connecting + expect(window.api.pty.kill).toHaveBeenCalledTimes(2) + expect(p.transport.getPtyId()).toBeNull() + } +) + +it.each(['reattach', 'cold-restore-new'] as const)( + 'ordinary remount preserves pending %s for adoption', + async (kind) => { + const p = await preparePendingSplitClose() + p.transport.detach?.({ preserveExitObserver: false }) + p.spawn.resolve(replyFor(kind)) + await p.connecting + expect(window.api.pty.kill).not.toHaveBeenCalled() + expect(p.state.terminalLayoutsByTabId[p.tabId].ptyIdsByLeafId?.[p.leafId]).toBe('pty-restored') + const { createIpcPtyTransport } = await import('./pty-transport') + vi.mocked(window.api.pty.spawn).mockResolvedValueOnce({ id: 'pty-restored', isReattach: true }) + const replacement = createIpcPtyTransport({}) + await replacement.connect({ url: '', sessionId: 'pty-restored', callbacks: {} }) + expect(replacement.getPtyId()).toBe('pty-restored') + replacement.detach?.({ preserveExitObserver: false }) + } +) + +it.each(['layout', 'transport'] as const)( + 'protects a sibling owner recorded only in %s', + async (owner) => { + const p = await preparePendingSplitClose() + const { createIpcPtyTransport } = await import('./pty-transport') + const survivor = createIpcPtyTransport({}) + if (owner === 'transport') { + survivor.attach({ existingPtyId: 'pty-restored', callbacks: {} }) + p.transports.set(2, survivor) + } else { + p.state.terminalLayoutsByTabId[p.tabId].ptyIdsByLeafId = { + [p.leafId]: 'pty-restored', + [p.siblingLeafId]: 'pty-restored' + } + } + p.actions.executeClosePane(1) + p.spawn.resolve(replyFor('reattach')) + await p.connecting + expect(window.api.pty.kill).not.toHaveBeenCalled() + survivor.detach?.({ preserveExitObserver: false }) + } +) + +it('protects a different tab owner present before explicit close', async () => { + const p = await preparePendingSplitClose() + p.state.tabsByWorktree.workspace.push(makeCloseTestTab('survivor', 'pty-restored')) + p.actions.executeClosePane(1) + p.spawn.resolve(replyFor('cold-restore-new')) + await p.connecting + expect(window.api.pty.kill).not.toHaveBeenCalled() +}) + +it.each(['same-leaf', 'different-tab', 'new-transport-map'] as const)( + 'protects a %s replacement that adopts after explicit close', + async (owner) => { + const p = await preparePendingSplitClose() + p.actions.executeClosePane(1) + const { createIpcPtyTransport } = await import('./pty-transport') + const replacement = createIpcPtyTransport({}) + if (owner === 'same-leaf') { + p.state.terminalLayoutsByTabId[p.tabId].ptyIdsByLeafId = { [p.leafId]: 'pty-restored' } + } else if (owner === 'different-tab') { + p.state.tabsByWorktree.workspace.push(makeCloseTestTab('new-owner', 'pty-restored')) + } else { + replacement.attach({ existingPtyId: 'pty-restored', callbacks: {} }) + p.controller.paneTransportsRef.current = new Map([[1, replacement]]) + } + p.spawn.resolve(replyFor('reattach')) + await p.connecting + expect(window.api.pty.kill).toHaveBeenCalledTimes(1) + replacement.detach?.({ preserveExitObserver: false }) + } +) + +it('retains explicit close intent across repeated generic destroy calls', async () => { + const p = await preparePendingSplitClose() + p.actions.executeClosePane(1) + p.transport.destroy?.() + p.transport.destroy?.() + p.spawn.resolve(replyFor('cold-restore-new')) + await p.connecting + expect(window.api.pty.kill).toHaveBeenCalledTimes(2) +}) + +it('preserves a different returned reattach identity', async () => { + const p = await preparePendingSplitClose() + p.actions.executeClosePane(1) + p.spawn.resolve({ id: 'different-existing-session', isReattach: true }) + await p.connecting + expect(window.api.pty.kill).toHaveBeenCalledExactlyOnceWith('pty-restored') +}) + +it('retries an eager provider failure when the same-ID reply arrives', async () => { + const p = await preparePendingSplitClose() + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + vi.mocked(window.api.pty.kill).mockRejectedValueOnce( + new Error('provider temporarily unavailable') + ) + p.actions.executeClosePane(1) + await flushPtySideEffects() + expect(warn).toHaveBeenCalledWith( + '[terminal-retirement] provider teardown failed', + expect.any(Object) + ) + p.spawn.resolve(replyFor('reattach')) + await p.connecting + expect(window.api.pty.kill).toHaveBeenCalledTimes(2) +}) + +it('does not invent a late session after a rejected spawn', async () => { + const p = await preparePendingSplitClose() + vi.spyOn(console, 'error').mockImplementation(() => {}) + p.actions.executeClosePane(1) + p.spawn.reject(new Error('spawn rejected')) + await p.connecting + expect(window.api.pty.kill).toHaveBeenCalledExactlyOnceWith('pty-restored') +}) + +it('routes direct SSH retirement through the existing IPC identity', async () => { + const id = 'ssh:ssh-1@@pty-restored' + const p = await preparePendingSplitClose(id) + p.state.worktreesByRepo = { repo: [{ id: 'workspace', repoId: 'repo', hostId: 'ssh:ssh-1' }] } + p.actions.executeClosePane(1) + p.spawn.resolve({ id, isReattach: true }) + await p.connecting + expect(window.api.pty.kill).toHaveBeenNthCalledWith(1, id) + expect(window.api.pty.kill).toHaveBeenNthCalledWith(2, id) +}) + +it.each(['runtime-handle', 'runtime-legacy', 'unresolved-owner', 'runtime-native-hint'] as const)( + 'never falls through to local kill for %s', + async (kind) => { + const id = + kind === 'runtime-handle' + ? 'remote:env-1@@pty-restored' + : kind === 'runtime-legacy' + ? 'remote:pty-restored' + : 'pty-restored' + const p = await preparePendingSplitClose(id) + if (kind === 'unresolved-owner') { + p.state.worktreesByRepo = {} + } else if (kind === 'runtime-native-hint') { + p.state.worktreesByRepo = { + repo: [ + { + id: 'workspace', + repoId: 'repo', + hostId: 'ssh:ssh-1', + runtimeOwnerEnvironmentId: 'env-1' + } + ] + } + } + p.actions.executeClosePane(1) + p.spawn.resolve({ id, isReattach: true }) + await p.connecting + expect(window.api.pty.kill).not.toHaveBeenCalled() + } +) + +it('retires a local folder-workspace split without a git worktree row', async () => { + const p = await preparePendingSplitClose('pty-restored', folderWorkspaceKey('folder-1')) + p.state.worktreesByRepo = {} + p.actions.executeClosePane(1) + p.spawn.resolve(replyFor('reattach')) + await p.connecting + expect(window.api.pty.kill).toHaveBeenCalledTimes(2) +}) 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/components/terminal-pane/pty-connection/hidden-output-restore-limits.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-limits.ts index 80ae6078f20..85336952549 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-limits.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-limits.ts @@ -35,3 +35,8 @@ export const HIDDEN_OUTPUT_RESTORE_LOCAL_GATE_MAX_ATTEMPTS = 30 // terminal state is unavailable, so the user has an explicit loss signal. export const HIDDEN_OUTPUT_RESTORE_UNAVAILABLE_WARNING = '\r\n[Orca skipped hidden terminal output because main recovery was unavailable.]\r\n' +// Why distinct from the warning above: that one closes a bounded retry the host kept declining +// (transient); this one reports a host that answered and can never produce the buffer for this +// request, so a blank or stale pane is not mistaken for an empty terminal. +export const PARK_REVEAL_NO_HOST_IMAGE_WARNING = + "\r\n[Orca could not restore this terminal's history from its host.]\r\n" diff --git a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-request.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-request.ts index 08946421c5d..e89c14f432a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-request.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-request.ts @@ -17,10 +17,12 @@ import { } from './hidden-output-restore-limits' import { shouldWritePtyOutputForeground } from './foreground-output-scan' import { restoredSnapshotPaintsPrintableContent } from '../restored-snapshot-coverage' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import type { ConnectPanePtySession } from './connect-pane-pty-session' -import type { HiddenOutputSnapshotResult } from './hidden-output-snapshot-serialize' +import { + classifyHiddenOutputSnapshotReject, + type HiddenOutputSnapshotResult +} from './hidden-output-snapshot-serialize' export function bindHiddenOutputRestoreRequest(session: ConnectPanePtySession): void { session.requestHiddenOutputRestoreIfNeeded = function (opts?: { @@ -138,13 +140,7 @@ export function bindHiddenOutputRestoreRequest(session: ConnectPanePtySession): ) }) } catch { - snapshotResult = - !isRemoteRuntimePtyId(currentPtyId) || - session.hiddenOutputRestoreLegacyPtyId === currentPtyId || - typeof session.transport.serializeBufferOutcome !== 'function' - ? { kind: 'unavailable' } - : // Why 'host': the only reject here is the request timeout — the frame went out and the host stayed silent. - { kind: 'retry-worthy', source: 'host' } + snapshotResult = classifyHiddenOutputSnapshotReject(session, currentPtyId) } if (session.disposed) { return diff --git a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-snapshot-serialize.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-snapshot-serialize.ts index 8629163d09e..e44c7a1661b 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-snapshot-serialize.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-snapshot-serialize.ts @@ -3,6 +3,7 @@ import { onTerminalScrollIntentFollowOutput } from '@/lib/pane-manager/terminal- import { shouldWritePtyOutputForeground } from './foreground-output-scan' import { readE2eHiddenSnapshotOverride } from './e2e-terminal-pty-harness' +import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import type { PtyBufferSnapshot } from '../pty-transport' import type { ConnectPanePtySession } from './connect-pane-pty-session' @@ -14,6 +15,18 @@ export type HiddenOutputSnapshotResult = | { kind: 'unknown-legacy-host' } | { kind: 'unavailable' } +/** Why 'host': on a modern remote transport the only reject is the request timeout — the frame went out and the host stayed silent. Elsewhere local main answered nothing. */ +export function classifyHiddenOutputSnapshotReject( + session: ConnectPanePtySession, + ptyId: string +): HiddenOutputSnapshotResult { + return !isRemoteRuntimePtyId(ptyId) || + session.hiddenOutputRestoreLegacyPtyId === ptyId || + typeof session.transport.serializeBufferOutcome !== 'function' + ? { kind: 'unavailable' } + : { kind: 'retry-worthy', source: 'host' } +} + export function bindSerializeHiddenOutputSnapshot(session: ConnectPanePtySession): void { session.serializeHiddenOutputSnapshot = async function ( ptyId: string, diff --git a/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.test.ts new file mode 100644 index 00000000000..6b3c263c814 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.test.ts @@ -0,0 +1,183 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + bindParkRevealSnapshotVerdictActions, + classifyParkRevealSnapshot +} from './park-reveal-snapshot-verdict' +import { PARK_REVEAL_NO_HOST_IMAGE_WARNING } from './hidden-output-restore-limits' +import type { ConnectPanePtySession } from './connect-pane-pty-session' + +const writeTerminalOutput = vi.hoisted(() => vi.fn()) + +vi.mock('../terminal-freeze-breadcrumbs', () => ({ recordTerminalFreezeBreadcrumb: vi.fn() })) +vi.mock('@/lib/pane-manager/pane-terminal-output-scheduler', () => ({ writeTerminalOutput })) + +const IMAGE = { data: 'PROMPT $ ', cols: 80, rows: 24, seq: 7, source: 'headless' as const } +const REMOTE_PTY_ID = 'remote:env-1@@pty-1' +const LOCAL_PTY_ID = 'wt-1@@local-pty-1' + +describe('classifyParkRevealSnapshot', () => { + it('reads a non-empty host image as positive evidence', () => { + expect( + classifyParkRevealSnapshot({ kind: 'snapshot', snapshot: IMAGE }, REMOTE_PTY_ID) + ).toEqual({ + kind: 'host-snapshot', + snapshot: IMAGE + }) + }) + + it('reads an alt-screen image as positive evidence even when its data is empty', () => { + const snapshot = { ...IMAGE, data: '', alternateScreen: true } + expect(classifyParkRevealSnapshot({ kind: 'snapshot', snapshot }, REMOTE_PTY_ID)).toEqual({ + kind: 'host-snapshot', + snapshot + }) + }) + + // A remote host's fallback serializer answers `data: ''` with no `unavailable` reason before + // its pane has hydrated. applyMainBufferSnapshot already refuses to paint that; so must the reveal. + it('reads a remote imageless success as unverifiable, charged to the host budget', () => { + expect( + classifyParkRevealSnapshot( + { kind: 'snapshot', snapshot: { ...IMAGE, data: '' } }, + REMOTE_PTY_ID + ) + ).toEqual({ kind: 'unverifiable', ledger: 'host' }) + }) + + it('reads an empty local-main model as positive evidence: local main is the execution host', () => { + const snapshot = { ...IMAGE, data: '' } + expect(classifyParkRevealSnapshot({ kind: 'snapshot', snapshot }, LOCAL_PTY_ID)).toEqual({ + kind: 'host-snapshot', + snapshot + }) + }) + + it.each([ + ['host silent past the request timeout', 'host'], + ['request lane gated before any frame left the client', 'local'] + ] as const)('reads retry-worthy (%s) as unverifiable on the %s ledger', (_label, source) => { + expect(classifyParkRevealSnapshot({ kind: 'retry-worthy', source }, REMOTE_PTY_ID)).toEqual({ + kind: 'unverifiable', + ledger: source + }) + }) + + it('reads a legacy host empty reply as unverifiable with no ledger charge', () => { + expect(classifyParkRevealSnapshot({ kind: 'unknown-legacy-host' }, REMOTE_PTY_ID)).toEqual({ + kind: 'unverifiable', + ledger: null + }) + }) + + // Retention is never inferred from image content: this arm is reachable only from an + // explicit host answer, and it carries which one so a caller need not re-derive it. + it.each(['permanently-unavailable', 'unavailable'] as const)( + 'reads %s as no host image, carrying the answer that closed the door', + (kind) => { + expect(classifyParkRevealSnapshot({ kind }, REMOTE_PTY_ID)).toEqual({ + kind: 'no-host-image', + reason: kind + }) + } + ) +}) + +function buildSession(overrides: Record = {}): ConnectPanePtySession { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a deliberately partial session bag; only the members the verdict actions touch exist, so an unexpected access fails the test. + const session = { + disposed: false, + transport: { getPtyId: () => REMOTE_PTY_ID }, + pane: { terminal: { id: 'xterm-1' } }, + beforeTerminalOutputWrite: vi.fn(), + canUseHiddenOutputSnapshot: () => true, + hiddenOutputRestoreRemoteOutcomeAttempts: 0, + hiddenOutputRestoreLocalGateAttempts: 0, + markHiddenOutputRestoreNeeded: vi.fn(), + ...overrides + } as unknown as ConnectPanePtySession + bindParkRevealSnapshotVerdictActions(session) + return session +} + +describe('warnParkRevealNoHostImage', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('writes the no-host-image banner into a remote pane, pen reset, no CAN byte', () => { + const session = buildSession() + + expect(session.warnParkRevealNoHostImage(REMOTE_PTY_ID, 'unavailable')).toBe(true) + + expect(writeTerminalOutput).toHaveBeenCalledExactlyOnceWith( + session.pane.terminal, + `\x1b[0m${PARK_REVEAL_NO_HOST_IMAGE_WARNING}`, + { foreground: true, beforeWrite: session.beforeTerminalOutputWrite } + ) + expect(writeTerminalOutput.mock.calls[0]?.[1]).not.toContain('\x18') + }) + + it('stays silent for a local pane, whose layout copy is never released', () => { + const session = buildSession({ transport: { getPtyId: () => LOCAL_PTY_ID } }) + + expect(session.warnParkRevealNoHostImage(LOCAL_PTY_ID, 'unavailable')).toBe(false) + + expect(writeTerminalOutput).not.toHaveBeenCalled() + }) + + it('does not banner a pane whose transport has moved on', () => { + const session = buildSession({ transport: { getPtyId: () => 'remote:env-1@@pty-2' } }) + + expect(session.warnParkRevealNoHostImage(REMOTE_PTY_ID, 'permanently-unavailable')).toBe(false) + + expect(writeTerminalOutput).not.toHaveBeenCalled() + }) +}) + +describe('retryUnverifiableParkRevealSnapshot', () => { + it('charges the reveal probe to the host budget and hands off to the restore loop', () => { + const session = buildSession() + + expect(session.retryUnverifiableParkRevealSnapshot('remote:env-1@@pty-1', 'host')).toBe(true) + + expect(session.hiddenOutputRestoreRemoteOutcomeAttempts).toBe(1) + expect(session.hiddenOutputRestoreLocalGateAttempts).toBe(0) + expect(session.markHiddenOutputRestoreNeeded).toHaveBeenCalledOnce() + }) + + it('charges a local gate to the local budget only', () => { + const session = buildSession() + + session.retryUnverifiableParkRevealSnapshot('remote:env-1@@pty-1', 'local') + + expect(session.hiddenOutputRestoreRemoteOutcomeAttempts).toBe(0) + expect(session.hiddenOutputRestoreLocalGateAttempts).toBe(1) + expect(session.markHiddenOutputRestoreNeeded).toHaveBeenCalledOnce() + }) + + it('charges nothing for a legacy host but still hands off', () => { + const session = buildSession() + + session.retryUnverifiableParkRevealSnapshot('remote:env-1@@pty-1', null) + + expect(session.hiddenOutputRestoreRemoteOutcomeAttempts).toBe(0) + expect(session.hiddenOutputRestoreLocalGateAttempts).toBe(0) + expect(session.markHiddenOutputRestoreNeeded).toHaveBeenCalledOnce() + }) + + it.each([ + [ + 'the transport moved to another PTY', + { transport: { getPtyId: () => 'remote:env-1@@pty-2' } } + ], + ['the session is disposed', { disposed: true }], + ['no hidden snapshot source exists', { canUseHiddenOutputSnapshot: () => false }] + ])('does nothing when %s', (_label, overrides) => { + const session = buildSession(overrides) + + expect(session.retryUnverifiableParkRevealSnapshot('remote:env-1@@pty-1', 'host')).toBe(false) + + expect(session.hiddenOutputRestoreRemoteOutcomeAttempts).toBe(0) + expect(session.markHiddenOutputRestoreNeeded).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.ts b/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.ts new file mode 100644 index 00000000000..5bfa786d3bf --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.ts @@ -0,0 +1,120 @@ +import { RESET_GRAPHIC_RENDITION } from '../../../../../shared/terminal-mode-reset-profiles' +import { redactPtyIdForDiagnostics } from '../../../../../shared/pty-delivery-diagnostics' +import { writeTerminalOutput } from '@/lib/pane-manager/pane-terminal-output-scheduler' +import { recordTerminalFreezeBreadcrumb } from '../terminal-freeze-breadcrumbs' +import type { PtyBufferSnapshot } from '../pty-transport' + +import type { ConnectPanePtySession } from './connect-pane-pty-session' +import { PARK_REVEAL_NO_HOST_IMAGE_WARNING } from './hidden-output-restore-limits' +import type { HiddenOutputSnapshotResult } from './hidden-output-snapshot-serialize' +import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' + +/** Which restore budget an unverifiable probe is charged to; null when the loop keeps its own count (legacy hosts). */ +export type ParkRevealRetryLedger = 'host' | 'local' | null + +/** The explicit answer that closed the door: only these two ever reach `no-host-image`. */ +export type ParkRevealNoHostImageReason = 'permanently-unavailable' | 'unavailable' + +/** + * What a park-reveal's host snapshot probe proved. Three verdicts, no synonyms + * (docs/reference/ssh-execution-boundary.md): only `host-snapshot` is positive + * evidence of the pane's contents, and a probe that proves nothing must never + * be read as "the pane is empty". Retention is never inferred from image + * content: `no-host-image` is reachable only from an explicit host answer. + */ +export type ParkRevealSnapshotVerdict = + | { kind: 'host-snapshot'; snapshot: PtyBufferSnapshot } + /** Timeout, host declined for now, local request-lane gate, or an imageless success. */ + | { kind: 'unverifiable'; ledger: ParkRevealRetryLedger } + /** The host answered, and repeating this request can never yield the buffer. */ + | { kind: 'no-host-image'; reason: ParkRevealNoHostImageReason } + +export function classifyParkRevealSnapshot( + result: HiddenOutputSnapshotResult, + ptyId: string +): ParkRevealSnapshotVerdict { + switch (result.kind) { + case 'snapshot': { + const { snapshot } = result + // Why remote-only: a remote host's fallback serializer can answer `data: ''` before its + // pane has hydrated, so an imageless success proves nothing there (applyMainBufferSnapshot + // refuses it too) and painting it would clear the client's own copy. Local main is the + // execution host for a local pty; its empty model is the positive answer. + const carriesNoImage = + isRemoteRuntimePtyId(ptyId) && + snapshot.alternateScreen !== true && + snapshot.data === '' && + !snapshot.scrollbackAnsi + return carriesNoImage + ? { kind: 'unverifiable', ledger: 'host' } + : { kind: 'host-snapshot', snapshot } + } + case 'retry-worthy': + return { kind: 'unverifiable', ledger: result.source } + case 'unknown-legacy-host': + return { kind: 'unverifiable', ledger: null } + case 'permanently-unavailable': + case 'unavailable': + return { kind: 'no-host-image', reason: result.kind } + } +} + +export function bindParkRevealSnapshotVerdictActions(session: ConnectPanePtySession): void { + // Why a hand-off, not a loop: the hidden-output restore loop already budgets + // retry-worthy answers (7 host declines / 30 local gates / 5 re-arm cycles), + // repaints from the host on success, and ends in an explicit loss banner. + // A second loop here would double every bound. + session.retryUnverifiableParkRevealSnapshot = function ( + ptyId: string, + ledger: ParkRevealRetryLedger + ): boolean { + if ( + session.disposed || + session.transport.getPtyId() !== ptyId || + !session.canUseHiddenOutputSnapshot(ptyId) + ) { + return false + } + // Charge the reveal's own probe so the shared budget counts it: 1 + 6 = 7 host requests. + if (ledger === 'host') { + session.hiddenOutputRestoreRemoteOutcomeAttempts += 1 + } else if (ledger === 'local') { + session.hiddenOutputRestoreLocalGateAttempts += 1 + } + recordTerminalFreezeBreadcrumb('park-reveal-unverifiable', { + id: redactPtyIdForDiagnostics(ptyId), + ledger: ledger ?? 'none' + }) + session.markHiddenOutputRestoreNeeded() + return true + } + + // Unavoidable loss must be visible: a pane the host could not image must not + // look like an empty terminal. Remote-only because local main is the + // execution host and a local pane's layout copy is never released, so the + // pane already shows everything there is. + session.warnParkRevealNoHostImage = function ( + ptyId: string, + reason: ParkRevealNoHostImageReason + ): boolean { + recordTerminalFreezeBreadcrumb('park-reveal-no-host-image', { + id: redactPtyIdForDiagnostics(ptyId), + reason + }) + if ( + !isRemoteRuntimePtyId(ptyId) || + session.disposed || + session.transport.getPtyId() !== ptyId + ) { + return false + } + // Why no CAN byte here: the subscribe-time push snapshot may have left a pending escape + // tail for the next live chunk to complete; only the pen is reset. + writeTerminalOutput( + session.pane.terminal, + `${RESET_GRAPHIC_RENDITION}${PARK_REVEAL_NO_HOST_IMAGE_WARNING}`, + { foreground: true, beforeWrite: session.beforeTerminalOutputWrite } + ) + return true + } +} diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler-park-reveal-verdict.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler-park-reveal-verdict.test.ts new file mode 100644 index 00000000000..86237ac8f21 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler-park-reveal-verdict.test.ts @@ -0,0 +1,335 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { bindHandleReattachResult } from './reattach-result-handler' +import { bindSerializeHiddenOutputSnapshot } from './hidden-output-snapshot-serialize' +import type { ConnectPanePtySession } from './connect-pane-pty-session' +import type { HiddenOutputSnapshotResult } from './hidden-output-snapshot-serialize' +import type { ReattachPayloadContext } from './reattach-payload-context' + +/** + * A park-reveal of a remote-runtime pty arrives with `replay: ''`, so the host + * snapshot probe is the ONLY structural paint. The probe has three answers and + * the handler must keep them apart (docs/reference/ssh-execution-boundary.md): + * + * host image → paint it (the structural clear + repaint) + * unverifiable → paint nothing AND re-ask the host, bounded + * no host image → paint nothing, ask nothing + * + * These tests assert on the decision the handler hands to the payload and the + * retry it arms, not on what the pane looks like: a live host also pushes a + * retained tail at subscribe time, so the rendered pane can look right whether + * or not the decision was. + */ +const REMOTE_PTY_ID = 'remote:env-1@@pty-1' +const HOST_IMAGE = { data: 'PROMPT $ ', cols: 80, rows: 24, seq: 7, source: 'headless' as const } + +const mocks = vi.hoisted(() => { + const state: Record = { tabsByWorktree: {}, terminalLayoutsByTabId: {} } + const capturedContexts: ReattachPayloadContext[] = [] + const callOrder: string[] = [] + return { state, capturedContexts, callOrder } +}) + +vi.mock('@/store', () => ({ + useAppStore: { getState: () => mocks.state } +})) +vi.mock('@/lib/codex-stale-pane-sweep', () => ({ notifyCodexPaneBoundForStaleSweep: vi.fn() })) +vi.mock('@/runtime/sync-runtime-graph', () => ({ scheduleRuntimeGraphSync: vi.fn() })) +vi.mock('../terminal-freeze-breadcrumbs', () => ({ recordTerminalFreezeBreadcrumb: vi.fn() })) +// Why: the payload handlers need a mounted xterm; what matters here is the +// context the handler decided on, so capture it and report the payload applied. +vi.mock('./apply-reattach-payload', () => ({ + createReattachPayloadHandlers: (_session: unknown, ctx: ReattachPayloadContext) => { + mocks.capturedContexts.push(ctx) + return { + applyReattachPayload: async () => { + mocks.callOrder.push('applyReattachPayload') + ctx.reattachPayloadApplied = true + }, + fitAfterReattachRestore: async () => { + mocks.callOrder.push('fitAfterReattachRestore') + } + } + } +})) + +type Bag = { + session: ConnectPanePtySession + transport: Record + /** The pane-transport registry, keyed by pane id; the bag is deliberately untyped. */ + paneTransports: Map + retryUnverifiableParkRevealSnapshot: ReturnType + warnParkRevealNoHostImage: ReturnType + structuralRun: ReturnType +} + +function buildParkRevealSession(overrides: Record = {}): Bag { + const transport: Record = { + getPtyId: () => REMOTE_PTY_ID, + disconnect: vi.fn(), + serializeBuffer: vi.fn() + } + const paneTransports = new Map([['pane-1', transport]]) + const warnParkRevealNoHostImage = vi.fn(() => { + mocks.callOrder.push('warnParkRevealNoHostImage') + return true + }) + const retryUnverifiableParkRevealSnapshot = vi.fn(() => { + mocks.callOrder.push('retryUnverifiableParkRevealSnapshot') + return true + }) + const structuralRun = vi.fn( + async ( + task: () => Promise, + opts?: { afterRestore?: () => Promise } + ): Promise => { + await task() + await opts?.afterRestore?.() + } + ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a deliberately partial session bag; only the members the park-reveal decision touches exist, so an unexpected access fails the test. + const session = { + transport, + disposed: false, + transportStreamGeneration: 0, + authoritativeReattachGeneration: 0, + // The reveal remount of a parked pane; consume-once in the handler. + mountFollowsTerminalPark: true, + followsDirectSshReconnect: false, + connectionId: null, + cacheKey: 'cache-1', + directSshRetryAttempt: undefined, + capturedDirectSshRetryPtyAccepted: false, + pane: { id: 'pane-1', leafId: 'leaf-1', terminal: { options: { scrollback: 1000 } } }, + deps: { + tabId: 'tab-1', + worktreeId: 'wt-1', + paneTransportsRef: { current: paneTransports }, + isVisibleRef: { current: true }, + clearTabPtyId: vi.fn(), + updateTabPtyId: vi.fn(), + restoredLeafId: null + }, + agentCompletionCoordinator: { startProcessTracking: vi.fn() }, + structuralReplayCoordinator: { run: structuralRun }, + getSshMainModelSnapshotProbe: () => async () => null, + serializeHiddenOutputSnapshot: vi.fn(), + retryUnverifiableParkRevealSnapshot, + warnParkRevealNoHostImage, + rejectObsoleteDirectSshReattach: () => false, + registerEffectiveLaunchConfig: vi.fn(), + clearExitedPanePtyLayoutBinding: vi.fn(), + syncPanePtyLayoutBinding: vi.fn(), + startFreshColdRestoreAgentResume: vi.fn(), + setPanePtyFitBinding: vi.fn(), + reportPanePtyVisibility: vi.fn(), + registerSideEffectFactConsumerForPty: vi.fn(), + syncHiddenRendererPtyDelivery: vi.fn(), + registerPaneSerializerFor: vi.fn(), + sampleVisiblePaneForegroundAgent: vi.fn(), + scheduleReattachIdleAgentCursorReset: vi.fn(), + settlePaneAttachAttempt: vi.fn(), + ...overrides + } as unknown as ConnectPanePtySession + bindHandleReattachResult(session) + return { + session, + transport, + paneTransports, + retryUnverifiableParkRevealSnapshot, + warnParkRevealNoHostImage, + structuralRun + } +} + +/** The remote transport's park-reveal result: a bare reattach with no relay tail. */ +const PARK_REVEAL_RESULT = { id: REMOTE_PTY_ID, replay: '', isReattach: true } + +function probeAnswers(result: HiddenOutputSnapshotResult): ReturnType { + return vi.fn(async () => result) +} + +function decidedContext(): ReattachPayloadContext { + expect(mocks.capturedContexts).toHaveLength(1) + return mocks.capturedContexts[0]! +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.state = { tabsByWorktree: {}, terminalLayoutsByTabId: {} } + mocks.capturedContexts = [] + mocks.callOrder = [] +}) + +describe('handleReattachResult park-reveal snapshot verdict', () => { + it('paints a host image and arms no retry', async () => { + const bag = buildParkRevealSession({ + serializeHiddenOutputSnapshot: probeAnswers({ kind: 'snapshot', snapshot: HOST_IMAGE }) + }) + + await expect(bag.session.handleReattachResult(PARK_REVEAL_RESULT)).resolves.toBe(true) + + const ctx = decidedContext() + expect(ctx.prefetchedParkModelSnapshot).toBe(HOST_IMAGE) + expect(ctx.shouldApplyStructuralPayload).toBe(true) + expect(bag.structuralRun).toHaveBeenCalledOnce() + expect(bag.retryUnverifiableParkRevealSnapshot).not.toHaveBeenCalled() + }) + + // The defect: a host that stayed silent past the request timeout used to + // collapse to the same null as "nothing to paint" and was never asked again. + it('does not paint blank when the host stayed silent, and re-asks it', async () => { + const bag = buildParkRevealSession({ + serializeHiddenOutputSnapshot: probeAnswers({ kind: 'retry-worthy', source: 'host' }) + }) + + await expect(bag.session.handleReattachResult(PARK_REVEAL_RESULT)).resolves.toBe(true) + + const ctx = decidedContext() + expect(ctx.prefetchedParkModelSnapshot).toBeNull() + // No structural transaction means no `\x1b[2J\x1b[3J` clear over the client's own copy. + expect(ctx.shouldApplyStructuralPayload).toBe(false) + expect(bag.structuralRun).not.toHaveBeenCalled() + expect(bag.retryUnverifiableParkRevealSnapshot).toHaveBeenCalledExactlyOnceWith( + REMOTE_PTY_ID, + 'host' + ) + // The retry's own repaint must queue behind this attempt, never nest inside it. + expect(mocks.callOrder).toEqual([ + 'applyReattachPayload', + 'fitAfterReattachRestore', + 'retryUnverifiableParkRevealSnapshot' + ]) + }) + + it('treats a rejected probe on a modern remote transport as the host staying silent', async () => { + const bag = buildParkRevealSession({ + hiddenOutputRestoreLegacyPtyId: null, + serializeHiddenOutputSnapshot: vi.fn(async () => { + throw new Error('Remote terminal snapshot timed out.') + }) + }) + bag.transport.serializeBufferOutcome = vi.fn() + + await expect(bag.session.handleReattachResult(PARK_REVEAL_RESULT)).resolves.toBe(true) + + expect(decidedContext().prefetchedParkModelSnapshot).toBeNull() + expect(bag.retryUnverifiableParkRevealSnapshot).toHaveBeenCalledExactlyOnceWith( + REMOTE_PTY_ID, + 'host' + ) + }) + + // Through the real serializer: the host answered `unavailable: 'no-serializable-buffer'`, + // whose own host-side comment reads "not proof the pane is empty". + it("re-asks a host that answered 'no-serializable-buffer' instead of painting blank", async () => { + const bag = buildParkRevealSession({ + canUseMainBufferSnapshot: () => false, + hiddenOutputRestoreLegacyPtyId: null + }) + bag.transport.serializeBufferOutcome = vi.fn(async () => ({ + availability: { kind: 'retry-worthy', cause: 'host-no-serializable-buffer' }, + snapshot: null + })) + bindSerializeHiddenOutputSnapshot(bag.session) + + await expect(bag.session.handleReattachResult(PARK_REVEAL_RESULT)).resolves.toBe(true) + + expect(bag.transport.serializeBufferOutcome).toHaveBeenCalledOnce() + const ctx = decidedContext() + expect(ctx.prefetchedParkModelSnapshot).toBeNull() + expect(ctx.shouldApplyStructuralPayload).toBe(false) + expect(bag.retryUnverifiableParkRevealSnapshot).toHaveBeenCalledExactlyOnceWith( + REMOTE_PTY_ID, + 'host' + ) + }) + + it('does not paint an imageless success over the client copy, and re-asks', async () => { + const bag = buildParkRevealSession({ + serializeHiddenOutputSnapshot: probeAnswers({ + kind: 'snapshot', + snapshot: { ...HOST_IMAGE, data: '' } + }) + }) + + await expect(bag.session.handleReattachResult(PARK_REVEAL_RESULT)).resolves.toBe(true) + + expect(decidedContext().prefetchedParkModelSnapshot).toBeNull() + expect(bag.structuralRun).not.toHaveBeenCalled() + expect(bag.retryUnverifiableParkRevealSnapshot).toHaveBeenCalledExactlyOnceWith( + REMOTE_PTY_ID, + 'host' + ) + }) + + it('charges a local request-lane gate to the local budget', async () => { + const bag = buildParkRevealSession({ + serializeHiddenOutputSnapshot: probeAnswers({ kind: 'retry-worthy', source: 'local' }) + }) + + await bag.session.handleReattachResult(PARK_REVEAL_RESULT) + + expect(bag.retryUnverifiableParkRevealSnapshot).toHaveBeenCalledExactlyOnceWith( + REMOTE_PTY_ID, + 'local' + ) + }) + + // Unavoidable loss, made visible: the host answered and cannot produce the buffer, so the + // pane must say so rather than pass for an empty terminal — and never ask again. + it.each(['permanently-unavailable', 'unavailable'] as const)( + 'paints nothing, asks nothing, and banners the loss when the host answered %s', + async (kind) => { + const bag = buildParkRevealSession({ serializeHiddenOutputSnapshot: probeAnswers({ kind }) }) + + await expect(bag.session.handleReattachResult(PARK_REVEAL_RESULT)).resolves.toBe(true) + + const ctx = decidedContext() + expect(ctx.prefetchedParkModelSnapshot).toBeNull() + expect(ctx.shouldApplyStructuralPayload).toBe(false) + expect(bag.retryUnverifiableParkRevealSnapshot).not.toHaveBeenCalled() + expect(bag.warnParkRevealNoHostImage).toHaveBeenCalledExactlyOnceWith(REMOTE_PTY_ID, kind) + expect(mocks.callOrder).toEqual([ + 'applyReattachPayload', + 'fitAfterReattachRestore', + 'warnParkRevealNoHostImage' + ]) + } + ) + + it.each([ + ['a host image', { kind: 'snapshot', snapshot: HOST_IMAGE }], + ['an unverifiable answer', { kind: 'retry-worthy', source: 'host' }] + ] as const)('does not banner the loss on %s', async (_label, result) => { + const bag = buildParkRevealSession({ serializeHiddenOutputSnapshot: probeAnswers(result) }) + + await bag.session.handleReattachResult(PARK_REVEAL_RESULT) + + expect(bag.warnParkRevealNoHostImage).not.toHaveBeenCalled() + }) + + it('arms no retry for an attempt a remount superseded while the probe was in flight', async () => { + const bag = buildParkRevealSession() + bag.session.serializeHiddenOutputSnapshot = vi.fn(async () => { + // A successor mount registered its own transport before the probe settled. + bag.paneTransports.set('pane-1', { getPtyId: () => REMOTE_PTY_ID }) + return { kind: 'retry-worthy', source: 'host' } + }) + + await expect(bag.session.handleReattachResult(PARK_REVEAL_RESULT)).resolves.toBe(false) + + expect(mocks.capturedContexts).toHaveLength(0) + expect(bag.retryUnverifiableParkRevealSnapshot).not.toHaveBeenCalled() + }) + + it('probes only the first reattach of a reveal remount', async () => { + const probe = probeAnswers({ kind: 'retry-worthy', source: 'host' }) + const bag = buildParkRevealSession({ serializeHiddenOutputSnapshot: probe }) + + await bag.session.handleReattachResult(PARK_REVEAL_RESULT) + await bag.session.handleReattachResult(PARK_REVEAL_RESULT) + + expect(probe).toHaveBeenCalledOnce() + expect(bag.retryUnverifiableParkRevealSnapshot).toHaveBeenCalledOnce() + }) +}) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts index d485e526631..cbef66a0cfb 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts @@ -19,6 +19,15 @@ import type { ReattachPayloadContext } from './reattach-payload-context' import { createReattachPayloadHandlers } from './apply-reattach-payload' import type { ReattachPayloadSession } from './reattach-payload-session' import { recoverUnverifiableDirectSshReattach } from './direct-ssh-reattach-recovery' +import { + classifyHiddenOutputSnapshotReject, + type HiddenOutputSnapshotResult +} from './hidden-output-snapshot-serialize' +import { + classifyParkRevealSnapshot, + type ParkRevealNoHostImageReason, + type ParkRevealRetryLedger +} from './park-reveal-snapshot-verdict' type ReattachResultSession = ReattachPayloadSession & Pick< @@ -42,7 +51,9 @@ type ReattachResultSession = ReattachPayloadSession & | 'registerSideEffectFactConsumerForPty' | 'rejectObsoleteDirectSshReattach' | 'reportPanePtyVisibility' + | 'retryUnverifiableParkRevealSnapshot' | 'sampleVisiblePaneForegroundAgent' + | 'warnParkRevealNoHostImage' | 'scheduleReattachIdleAgentCursorReset' | 'serializeHiddenOutputSnapshot' | 'settlePaneAttachAttempt' @@ -280,19 +291,32 @@ export function bindHandleReattachResult(sessionBag: ConnectPanePtySession): voi // host snapshot before releasing queued live bytes; null falls back to // the subscribe screen without keeping the old xterm mounted. let prefetchedParkModelSnapshot: PtyBufferSnapshot | null = null + // Why kept apart from null: null means "paint nothing", never "the pane is + // empty". A probe that proved nothing (timeout, host declined for now) must + // also re-ask the host, bounded, once the payload has settled. + let unverifiableParkRevealLedger: ParkRevealRetryLedger | undefined + let noHostImageReason: ParkRevealNoHostImageReason | undefined if (revealFollowsTerminalPark && (!hasStructuralReplay || isRemoteRuntimePtyId(ptyId))) { if (parseAppSshPtyId(ptyId)) { prefetchedParkModelSnapshot = await fetchSshMainModelReattachSnapshot() } else { + let result: HiddenOutputSnapshotResult try { - const result = await session.serializeHiddenOutputSnapshot(ptyId, { + result = await session.serializeHiddenOutputSnapshot(ptyId, { scrollbackRows: resolveHiddenRestoreScrollbackRows( session.pane.terminal.options.scrollback ) }) - prefetchedParkModelSnapshot = result.kind === 'snapshot' ? result.snapshot : null } catch { - prefetchedParkModelSnapshot = null + result = classifyHiddenOutputSnapshotReject(sessionBag, ptyId) + } + const verdict = classifyParkRevealSnapshot(result, ptyId) + if (verdict.kind === 'host-snapshot') { + prefetchedParkModelSnapshot = verdict.snapshot + } else if (verdict.kind === 'unverifiable') { + unverifiableParkRevealLedger = verdict.ledger + } else { + noHostImageReason = verdict.reason } } if (!isCurrentReattachPayload()) { @@ -332,6 +356,12 @@ export function bindHandleReattachResult(sessionBag: ConnectPanePtySession): voi if (!isCurrentReattachPayload() || !reattachPayload.reattachPayloadApplied) { return false } + if (unverifiableParkRevealLedger !== undefined) { + // After the payload, so the retry's own structural repaint queues behind this attempt instead of nesting in it. + session.retryUnverifiableParkRevealSnapshot(ptyId, unverifiableParkRevealLedger) + } else if (noHostImageReason !== undefined) { + session.warnParkRevealNoHostImage(ptyId, noHostImageReason) + } session.scheduleReattachIdleAgentCursorReset() scheduleRuntimeGraphSync() diff --git a/src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts b/src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts index 3dc1c3aaef5..b02b9dfcef1 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts @@ -24,6 +24,7 @@ import { bindSettlePaneSerializer } from './pane-serializer-settle' import { bindDeferredColdRestoreAndSnapshot } from './deferred-cold-restore-and-snapshot' import { bindHiddenOutputSeqAndSkip } from './hidden-output-seq-and-skip' import { bindHiddenRestoreStateAndSshProbe } from './hidden-restore-state-and-ssh-probe' +import { bindParkRevealSnapshotVerdictActions } from './park-reveal-snapshot-verdict' export function installRunDeferredConnect(session: ConnectPanePtySession): void { const cwdPromise = session.deps.cwdPromise @@ -193,6 +194,7 @@ export function installRunDeferredConnect(session: ConnectPanePtySession): void bindHiddenRestoreStateAndSshProbe(session) bindPrepaintParkedSshSnapshot(session) + bindParkRevealSnapshotVerdictActions(session) bindHandleReattachResult(session) runDeferredSessionAttach(session) } 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/pty-transport-types.ts b/src/renderer/src/components/terminal-pane/pty-transport-types.ts index 4d7eaf7358b..2f6ed734bbf 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport-types.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport-types.ts @@ -232,7 +232,10 @@ export type PtyTransport = { * it also drops the transport's output processor from the pty side-effect memory census, * so a reattached one would run untracked. Create a new transport instead. */ detach?: (options?: { preserveExitObserver?: boolean }) => void - destroy?: () => void | Promise + destroy?: (options?: { + /** Explicit close can retain retirement intent until an unbound connect settles. */ + onAbandonedConnect?: (ptyId: string) => boolean + }) => void | Promise } export type IpcPtyTransportOptions = { diff --git a/src/renderer/src/components/terminal-pane/pty-transport.ts b/src/renderer/src/components/terminal-pane/pty-transport.ts index f794b9a1e4d..7731e75eac5 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport.ts @@ -44,6 +44,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra } = opts let connected = false let destroyed = false + let onAbandonedConnect: ((ptyId: string) => boolean) | undefined let ptyId: string | null = null let lifecycleGeneration = 0 let lastExitGeneration: number | null = null @@ -137,6 +138,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra lastExitGeneration === lifecycleGeneration && lifecycleGeneration === connectGeneration + 1, ownsPtyId: (id) => !destroyed && connected && ptyId === id, + handleExplicitlyClosedConnect: (id) => destroyed && (onAbandonedConnect?.(id) ?? false), bind, isCurrent: (id) => lifecycleGeneration === connectGeneration && connected && ptyId === id, setCallbacks, @@ -268,7 +270,8 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra : { ...(opts.cwd ? { cwd: opts.cwd } : {}), ...(shellOverride ? { shellOverride } : {}) }, resetCrossChunkParserState: outputProcessor.resetAgentStatusCarry, - destroy() { + destroy(options) { + onAbandonedConnect ??= options?.onAbandonedConnect destroyed = true try { this.disconnect() diff --git a/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts b/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts new file mode 100644 index 00000000000..60e7079ec94 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts @@ -0,0 +1,279 @@ +import type * as React from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' +import { createDeferred, flushAsyncTicks } from './pty-connection-test-async' +import { + LEAF_1, + createMockTransport, + createPane, + captureCallbackTerminalWrites, + createManager, + type MockPane, + type MockPaneManager, + type MockTransport +} from './pty-connection-test-pane-fixtures' +import { buildPaneConnectionDeps, type PaneConnectionDeps } from './pty-connection-test-deps' +import { createInitialStoreState } from './pty-connection-test-store-fixtures' +import type { StoreState } from './pty-connection-test-store-state' +import { + installTerminalTestGlobals, + restoreTerminalTestGlobals +} from './pty-connection-test-environment' + +/** + * End to end through connectPanePty: a parked remote-runtime pane is revealed, + * the host answers its snapshot probe with `unavailable: 'no-serializable-buffer'` + * ("not proof the pane is empty", per the host's own comment), and the pane must + * keep asking on the hidden-output restore loop's budget rather than paint blank + * once and go quiet. The request count and the loss banner are the oracle: with + * the reveal collapsing the answer to null, the count stays at one forever and + * the banner never appears. + */ +const REMOTE_PTY_ID = 'remote:env-1@@pty-1' +const HOST_IMAGE_MARKER = 'HOST-IMAGE-AFTER-PARK' +const BANNER_FRAGMENT = 'main recovery was unavailable' +const STRUCTURAL_CLEAR = '\x1b[2J\x1b[3J' + +const { + resetAndRefreshAllTerminalWebglAtlases, + scheduleTerminalWebglAtlasRecovery, + scheduleRuntimeGraphSync, + shouldSeedCacheTimerOnInitialTitle, + toastInfo, + notifyCodexPaneBoundForStaleSweep +} = vi.hoisted(() => ({ + resetAndRefreshAllTerminalWebglAtlases: vi.fn(), + scheduleTerminalWebglAtlasRecovery: vi.fn(), + scheduleRuntimeGraphSync: vi.fn(), + shouldSeedCacheTimerOnInitialTitle: vi.fn(() => false), + toastInfo: vi.fn(), + notifyCodexPaneBoundForStaleSweep: vi.fn() +})) + +let mockStoreState: StoreState +let transportFactoryQueue: MockTransport[] = [] +let storeSubscribers: ((state: StoreState) => void)[] = [] + +vi.mock('@/runtime/sync-runtime-graph', () => ({ + scheduleRuntimeGraphSync +})) + +vi.mock('@/lib/pane-manager/pane-manager-registry', async (importOriginal) => ({ + ...(await importOriginal>()), + resetAndRefreshAllTerminalWebglAtlases +})) + +vi.mock('./terminal-webgl-atlas-recovery', () => ({ + scheduleTerminalWebglAtlasRecovery +})) + +vi.mock('@/store', () => ({ + useAppStore: { + getState: () => mockStoreState, + subscribe: (listener: (state: StoreState) => void) => { + storeSubscribers.push(listener) + return () => { + storeSubscribers = storeSubscribers.filter((candidate) => candidate !== listener) + } + } + } +})) + +vi.mock('@/lib/agent-status', async (importOriginal) => { + const { buildAgentStatusModuleMock } = await import('./pty-connection-test-environment') + return buildAgentStatusModuleMock(await importOriginal>()) +}) + +vi.mock('./cache-timer-seeding', () => ({ + shouldSeedCacheTimerOnInitialTitle +})) + +vi.mock('sonner', () => ({ + toast: { + info: toastInfo + } +})) + +vi.mock('@/lib/codex-stale-pane-sweep', () => ({ + notifyCodexPaneBoundForStaleSweep +})) + +vi.mock('react', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useCallback: unknown>(fn: T): T => fn + } +}) + +function nextQueuedTransport(): MockTransport { + const nextTransport = transportFactoryQueue.shift() + if (!nextTransport) { + throw new Error('No mock transport queued') + } + return nextTransport +} + +vi.mock('./pty-transport', () => ({ + createIpcPtyTransport: vi.fn(() => nextQueuedTransport()) +})) + +vi.mock('./remote-runtime-pty-transport', () => ({ + createRemoteRuntimePtyTransport: vi.fn(() => nextQueuedTransport()) +})) + +vi.mock('./pty-dispatcher', async (importOriginal) => { + const actual = await importOriginal>() + return { + ...actual, + getEagerPtyBufferHandle: vi.fn(() => undefined) + } +}) + +function createDeps(overrides: Record = {}) { + return buildPaneConnectionDeps(() => mockStoreState, overrides) +} + +/** The host advertised paired parking while it was reachable, so the reveal reattaches its pty. */ +function pairedParkingHostStatus(): StoreState['runtimeStatusByEnvironmentId'] { + return new Map([ + [ + 'env-1', + { checkedAt: 1, status: { capabilities: [TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY] } } + ] + ]) +} + +type ConnectMockPane = ( + pane: MockPane, + manager: MockPaneManager, + deps: PaneConnectionDeps +) => { dispose: () => void } + +async function revealParkedRemotePane( + serializeBufferOutcome: ReturnType +): Promise<{ transport: MockTransport; writes: string[]; dispose: () => void }> { + const { connectPanePty } = await import('./pty-connection') + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared mock pane, manager, and deps stand in for the real xterm-backed objects, exactly as every other connectPanePty suite drives them. + const connectMockPane = connectPanePty as unknown as ConnectMockPane + const transport = createMockTransport() + // The remote transport's park-reveal result: a bare reattach with no relay tail. + transport.connect.mockImplementation(async () => { + transport.getPtyId.mockReturnValue(REMOTE_PTY_ID) + return { id: REMOTE_PTY_ID, replay: '', isReattach: true } + }) + transport.serializeBuffer = vi.fn() + transport.serializeBufferOutcome = serializeBufferOutcome + transportFactoryQueue.push(transport) + mockStoreState = { + ...mockStoreState, + tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: REMOTE_PTY_ID }] }, + ptyIdsByTabId: { 'tab-1': [REMOTE_PTY_ID] }, + terminalLayoutsByTabId: { + 'tab-1': { + root: { type: 'leaf', leafId: LEAF_1 }, + activeLeafId: LEAF_1, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_1]: REMOTE_PTY_ID } + } + }, + runtimeStatusByEnvironmentId: pairedParkingHostStatus() + } + const pane = createPane(1) + const { writes } = captureCallbackTerminalWrites(pane) + // Why the active pane: an inactive split defers its restore to the frame scheduler. + const binding = connectMockPane( + pane, + createManager(1, 1), + createDeps({ + mountFollowsTerminalPark: true, + restoredLeafId: LEAF_1, + restoredPtyIdByLeafId: { [LEAF_1]: REMOTE_PTY_ID } + }) + ) + await flushAsyncTicks(30) + expect(transport.connect).toHaveBeenCalledWith( + expect.objectContaining({ sessionId: REMOTE_PTY_ID }) + ) + return { transport, writes, dispose: () => binding.dispose() } +} + +function bannerCount(writes: string[]): number { + return writes.filter((data) => data.includes(BANNER_FRAGMENT)).length +} + +describe('parked remote pane reveal with an unverifiable host snapshot', () => { + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + transportFactoryQueue = [] + storeSubscribers = [] + mockStoreState = createInitialStoreState(() => mockStoreState) + installTerminalTestGlobals() + }) + + afterEach(async () => { + vi.useRealTimers() + await restoreTerminalTestGlobals() + }) + + it("keeps asking a host that answered 'no-serializable-buffer' and banners once the budget is spent", async () => { + const declined = { + availability: { kind: 'retry-worthy', cause: 'host-no-serializable-buffer' }, + snapshot: null + } + // Why the deferred second answer: the loop arms its 2s re-ask timer when an + // answer lands, and that timer must be created under fake timers to advance. + const secondAnswer = createDeferred() + const serializeBufferOutcome = vi + .fn() + .mockResolvedValueOnce(declined) + .mockReturnValueOnce(secondAnswer.promise) + .mockResolvedValue(declined) + const reveal = await revealParkedRemotePane(serializeBufferOutcome) + + // The reveal's own probe, plus the immediate re-ask it hands to the restore loop. + expect(serializeBufferOutcome).toHaveBeenCalledTimes(2) + expect(reveal.writes.join('')).not.toContain(STRUCTURAL_CLEAR) + expect(bannerCount(reveal.writes)).toBe(0) + + vi.useFakeTimers() + secondAnswer.resolve(declined) + await flushAsyncTicks(20) + expect(serializeBufferOutcome).toHaveBeenCalledTimes(2) + // One shared budget: the reveal probe counts, so five more 2s cycles reach the seventh. + for (let expectedRequests = 3; expectedRequests <= 7; expectedRequests += 1) { + await vi.advanceTimersByTimeAsync(2_000) + await flushAsyncTicks(20) + expect(serializeBufferOutcome).toHaveBeenCalledTimes(expectedRequests) + expect(bannerCount(reveal.writes)).toBe(expectedRequests === 7 ? 1 : 0) + } + await vi.advanceTimersByTimeAsync(60_000) + await flushAsyncTicks(20) + expect(serializeBufferOutcome).toHaveBeenCalledTimes(7) + expect(bannerCount(reveal.writes)).toBe(1) + // At no point did the pane claim to be empty. + expect(reveal.writes.join('')).not.toContain(STRUCTURAL_CLEAR) + reveal.dispose() + }) + + it('paints the host image when the host answers with one, and asks nothing more', async () => { + const serializeBufferOutcome = vi.fn().mockResolvedValue({ + availability: { kind: 'snapshot' }, + snapshot: { data: `${HOST_IMAGE_MARKER}\r\n`, cols: 80, rows: 24, seq: 3, source: 'headless' } + }) + const reveal = await revealParkedRemotePane(serializeBufferOutcome) + + expect(serializeBufferOutcome).toHaveBeenCalledTimes(1) + const painted = reveal.writes.join('') + expect(painted).toContain(STRUCTURAL_CLEAR) + expect(painted).toContain(HOST_IMAGE_MARKER) + + vi.useFakeTimers() + await vi.advanceTimersByTimeAsync(60_000) + await flushAsyncTicks(20) + expect(serializeBufferOutcome).toHaveBeenCalledTimes(1) + expect(bannerCount(reveal.writes)).toBe(0) + reveal.dispose() + }) +}) diff --git a/src/renderer/src/components/terminal-pane/retire-unbound-ipc-terminal-pane.ts b/src/renderer/src/components/terminal-pane/retire-unbound-ipc-terminal-pane.ts new file mode 100644 index 00000000000..081a33fc895 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/retire-unbound-ipc-terminal-pane.ts @@ -0,0 +1,83 @@ +import type { AppState } from '@/store/types' +import { + buildTerminalTabRetirementPlan, + getTerminalPtyOwnershipIdentity, + hasTerminalPtyOwnerOutsidePane +} from '@/store/slices/terminal-tab-retirement' +import { startTerminalTabProviderRetirement } from '@/store/terminals/terminal-tab-close-providers' +import type { PtyTransport } from './pty-transport-types' + +/** Capture explicit split-close intent before the durable leaf binding is removed. */ +export function retireUnboundIpcTerminalPane(args: { + getState: () => AppState + tabId: string + leafId: string + transport: PtyTransport | undefined + getTransports: () => ReadonlyMap +}): void { + const { getState, tabId, leafId, transport, getTransports } = args + if (!transport || transport.getPtyId()) { + return + } + const state = getState() + const requestedPtyId = state.terminalLayoutsByTabId[tabId]?.ptyIdsByLeafId?.[leafId] + if (!requestedPtyId) { + return + } + const plan = buildTerminalTabRetirementPlan(state, tabId) + const identity = getTerminalPtyOwnershipIdentity(state, requestedPtyId, plan.worktreeId) + const ptyId = plan.localOrSshPtyIds.find( + (candidate) => getTerminalPtyOwnershipIdentity(state, candidate, plan.worktreeId) === identity + ) + // Paired-runtime handles and unresolved routes cannot authorize an IPC kill. + if (!ptyId) { + return + } + const hasOtherOwner = (excludedLeafId?: string): boolean => { + const current = getState() + return ( + hasTerminalPtyOwnerOutsidePane(current, identity, tabId, excludedLeafId) || + [...getTransports().values()].some((candidate) => { + const boundId = candidate.getPtyId() + return ( + boundId !== null && + getTerminalPtyOwnershipIdentity(current, boundId, plan.worktreeId) === identity + ) + }) + ) + } + if (hasOtherOwner(leafId)) { + return + } + const retirementPlan = { + ...plan, + ptyIds: [ptyId], + localOrSshPtyIds: [ptyId], + runtimeTerminals: [], + cleanupOnlyPtyIds: [], + sharedPtyIds: [], + unroutablePtyIds: [] + } + const requestRetirement = (): void => { + startTerminalTabProviderRetirement({ + localPtyTeardownOwnedExternally: false, + remoteCloseOwnedByHost: false, + retirementPlan, + state: getState(), + tabId + }) + } + transport.destroy?.({ + onAbandonedConnect: (returnedPtyId) => { + if (returnedPtyId !== requestedPtyId) { + return false + } + // A replacement can own even this same leaf by the time the reply arrives. + if (!hasOtherOwner()) { + requestRetirement() + } + return true + } + }) + requestRetirement() +} diff --git a/src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts b/src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts index 0d731bc07f0..37fe50db1a1 100644 --- a/src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts +++ b/src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts @@ -19,6 +19,12 @@ function sleepingRecord( } } +function stateWith(sleepingAgentSessionsByPaneKey: Record): { + sleepingAgentSessionsByPaneKey: Record +} { + return { sleepingAgentSessionsByPaneKey } +} + describe('selectSleepingRecordParkExemptTabIds', () => { it.each([ [`tab-1:${LEAF_ID}`, 'tab-1'], @@ -26,14 +32,16 @@ describe('selectSleepingRecordParkExemptTabIds', () => { ])('derives the owner from a valid pane key (%s)', (paneKey, tabId) => { const records = { [paneKey]: sleepingRecord({ paneKey }) } - expect([...selectSleepingRecordParkExemptTabIds(records, 'wt-1')]).toEqual([tabId]) + expect([...selectSleepingRecordParkExemptTabIds(stateWith(records), 'wt-1')]).toEqual([tabId]) }) it('prefers the persisted tab id over the pane key owner', () => { const paneKey = `tab-stale:${LEAF_ID}` const records = { [paneKey]: sleepingRecord({ paneKey, tabId: 'tab-current' }) } - expect([...selectSleepingRecordParkExemptTabIds(records, 'wt-1')]).toEqual(['tab-current']) + expect([...selectSleepingRecordParkExemptTabIds(stateWith(records), 'wt-1')]).toEqual([ + 'tab-current' + ]) }) it('does not invent an owner for a delimiter-less pane key', () => { @@ -41,6 +49,30 @@ describe('selectSleepingRecordParkExemptTabIds', () => { 'orphan-pane-key': sleepingRecord({ paneKey: 'orphan-pane-key' }) } - expect([...selectSleepingRecordParkExemptTabIds(records, 'wt-1')]).toEqual([]) + expect([...selectSleepingRecordParkExemptTabIds(stateWith(records), 'wt-1')]).toEqual([]) + }) + + it('rebuilds when the record map changes and reuses the result when it does not', () => { + const paneKey = `tab-1:${LEAF_ID}` + const records = { [paneKey]: sleepingRecord({ paneKey }) } + const state = stateWith(records) + + const first = selectSleepingRecordParkExemptTabIds(state, 'wt-1') + expect(selectSleepingRecordParkExemptTabIds(state, 'wt-1')).toBe(first) + + const nextPaneKey = `tab-2:${LEAF_ID}` + const grown = stateWith({ ...records, [nextPaneKey]: sleepingRecord({ paneKey: nextPaneKey }) }) + + expect([...selectSleepingRecordParkExemptTabIds(grown, 'wt-1')]).toEqual(['tab-1', 'tab-2']) + }) + + // Why: a memo that serves a stale generation after the workspace's records are + // dropped would pin a hidden pane mounted for the rest of the session. + it('drops a worktree exemption once its records leave the map', () => { + const paneKey = `tab-1:${LEAF_ID}` + const populated = stateWith({ [paneKey]: sleepingRecord({ paneKey }) }) + expect([...selectSleepingRecordParkExemptTabIds(populated, 'wt-1')]).toEqual(['tab-1']) + + expect([...selectSleepingRecordParkExemptTabIds(stateWith({}), 'wt-1')]).toEqual([]) }) }) diff --git a/src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.ts b/src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.ts index 5a2ccec0e2a..4859edade2b 100644 --- a/src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.ts +++ b/src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.ts @@ -1,41 +1,54 @@ import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume' import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../../shared/stable-pane-id' import { isPassiveCompletedHibernationEvidence } from '../../lib/sleeping-agent-pane-ownership' +import { createWorktreeRecordSelector } from '@/store/worktree-record-selector-cache' const EMPTY_TAB_IDS: ReadonlySet = new Set() +type SleepingRecordParkExemptionState = { + sleepingAgentSessionsByPaneKey?: Record +} + /** Tab ids whose panes own a sleeping record a mount can actually consume. * Why: a parked pane can never cold-restore, so per-tab parks must exempt * these — but only these: passive-completed records never resume, * and exempting them would pin a hidden pane mounted indefinitely. - * Callers subscribe through `useShallow`, which compares the set structurally, - * so a write for another worktree cannot re-render this one. Iterates in place — - * `Object.values` would allocate every record on every store write. */ -export function selectSleepingRecordParkExemptTabIds( - sleepingAgentSessionsByPaneKey: Record | undefined, - worktreeId: string -): ReadonlySet { - if (!sleepingAgentSessionsByPaneKey) { - return EMPTY_TAB_IDS + * + * Why memoized on the record map's identity (STA-7552): zustand re-runs every + * mounted subscriber's selector on every store write, so an unrelated pane + * title update used to walk the whole inventory once per retained worktree. + * The map changes only when a record is parked or consumed, so that identity + * is the exact gate. + * Iterates in place — `Object.values` would allocate every record per rebuild. */ +export const selectSleepingRecordParkExemptTabIds = createWorktreeRecordSelector< + SleepingRecordParkExemptionState, + ReadonlySet +>({ + readSources: (state) => [state.sleepingAgentSessionsByPaneKey], + empty: EMPTY_TAB_IDS, + build: ({ sleepingAgentSessionsByPaneKey }, worktreeId) => { + if (!sleepingAgentSessionsByPaneKey) { + return EMPTY_TAB_IDS + } + let owned: Set | null = null + for (const paneKey in sleepingAgentSessionsByPaneKey) { + const record = sleepingAgentSessionsByPaneKey[paneKey] + if (!record || record.worktreeId !== worktreeId) { + continue + } + if (isPassiveCompletedHibernationEvidence(record)) { + continue + } + // Why: malformed pane keys must yield no owner instead of a truncated tab id. + const tabId = + record.tabId ?? + parsePaneKey(record.paneKey)?.tabId ?? + parseLegacyNumericPaneKey(record.paneKey)?.tabId + if (tabId) { + owned ??= new Set() + owned.add(tabId) + } + } + return owned ?? EMPTY_TAB_IDS } - let owned: Set | null = null - for (const paneKey in sleepingAgentSessionsByPaneKey) { - const record = sleepingAgentSessionsByPaneKey[paneKey] - if (!record || record.worktreeId !== worktreeId) { - continue - } - if (isPassiveCompletedHibernationEvidence(record)) { - continue - } - // Why: malformed pane keys must yield no owner instead of a truncated tab id. - const tabId = - record.tabId ?? - parsePaneKey(record.paneKey)?.tabId ?? - parseLegacyNumericPaneKey(record.paneKey)?.tabId - if (tabId) { - owned ??= new Set() - owned.add(tabId) - } - } - return owned ?? EMPTY_TAB_IDS -} +}) 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/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 diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-close-admission.ts b/src/renderer/src/components/terminal-pane/terminal-pane-close-admission.ts new file mode 100644 index 00000000000..19ee58355c3 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-pane-close-admission.ts @@ -0,0 +1,68 @@ +import type { AppState } from '@/store/types' +import { + buildTerminalTabRetirementPlan, + getTerminalPtyOwnershipIdentity +} from '@/store/slices/terminal-tab-retirement' +import { locateTerminalTab } from '@/store/terminals/terminal-tab-location' +import { resolveTerminalHostOwnership } from '@/lib/terminal-worktree-route' +import { parseRemoteRuntimePtyId } from '@/runtime/runtime-terminal-stream' +import { getRuntimeEnvironmentRevision } from '@/runtime/runtime-environment-revision' +import type { TerminalPaneBindingController } from './use-terminal-pane-layout-bindings' + +export function capturePendingTerminalPaneClose( + controller: Pick, + paneId: number, + getState: () => AppState +): { ptyId: string; isCurrent: () => boolean } | undefined { + const { managerRef, paneTransportsRef, tabId } = controller + const manager = managerRef.current + const transport = paneTransportsRef.current.get(paneId) + const leafId = manager?.getLeafId(paneId) + const state = getState() + const ptyId = leafId && state.terminalLayoutsByTabId[tabId]?.ptyIdsByLeafId?.[leafId] + if (!manager || !transport || transport.getPtyId() || !leafId || !ptyId) { + return undefined + } + const plan = buildTerminalTabRetirementPlan(state, tabId) + const remote = parseRemoteRuntimePtyId(ptyId) + const environmentId = remote?.environmentId?.trim() + const owner = resolveTerminalHostOwnership(state, plan.worktreeId, 'teardown') + const identity = getTerminalPtyOwnershipIdentity(state, ptyId, plan.worktreeId) + const isIdentity = (id: string): boolean => + getTerminalPtyOwnershipIdentity(state, id, plan.worktreeId) === identity + if (!plan.localOrSshPtyIds.some(isIdentity)) { + return undefined + } + const originalTab = locateTerminalTab(state.tabsByWorktree, tabId)?.tab + const createdAt = originalTab?.createdAt + const generation = originalTab?.generation ?? 0 + const pairingRevision = environmentId ? getRuntimeEnvironmentRevision(environmentId) : undefined + return { + ptyId, + isCurrent: () => { + const current = getState() + const currentTab = locateTerminalTab(current.tabsByWorktree, tabId) + const currentOwner = resolveTerminalHostOwnership(current, plan.worktreeId, 'teardown') + const currentPtyId = current.terminalLayoutsByTabId[tabId]?.ptyIdsByLeafId?.[leafId] + const boundId = transport.getPtyId() + // A split confirmation cannot authorize a replacement pane or a later whole-tab close. + return ( + managerRef.current === manager && + manager.getPanes().length > 1 && + manager.getPanes().some((pane) => pane.id === paneId && pane.leafId === leafId) && + manager.getLeafId(paneId) === leafId && + paneTransportsRef.current.get(paneId) === transport && + currentTab?.worktreeId === plan.worktreeId && + currentTab?.tab.createdAt === createdAt && + (currentTab?.tab.generation ?? 0) === generation && + currentOwner.kind === owner.kind && + currentOwner.runtimeEnvironmentId === owner.runtimeEnvironmentId && + (!environmentId || getRuntimeEnvironmentRevision(environmentId) === pairingRevision) && + !!currentPtyId && + getTerminalPtyOwnershipIdentity(current, currentPtyId, plan.worktreeId) === identity && + (boundId === null || + getTerminalPtyOwnershipIdentity(current, boundId, plan.worktreeId) === identity) + ) + } + } +} diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts b/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts index 2eb22ecfb03..0757276aa2c 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts @@ -18,8 +18,12 @@ const TERMINAL_PANE_HOOK_SOURCE_PATTERN = // paused notice that read it (207 hooks, still 8 useMemo). // Then host-authoritative layout removal added two `useRef`s in reconciliation // (last host layout leaf set, retired leaf set) (209 hooks, still 8 useMemo). +// Then search match count + Cmd+F focus parity (#9035) added a `useRef` and a `useCallback` in +// foundation (search input ref, focus-search-input) (211 hooks, still 8 useMemo). +// Then the pending split-close admission added one `useRef` in close-actions +// (the confirmed-close continuation) (212 hooks, still 8 useMemo). const PRE_REFACTOR_HOOK_ORDER_SHA256 = - 'f6de13ab7d6d130444c50fec2cfe097851ee1b7ecf0f3a2cbdc082c2e8e8838b' + 'a3ec9b9faac724605fbf8ebe6647f65b185ed0e2b2e763282159b289f0d199c6' const sourceFiles = readdirSync(__dirname) .filter((name) => TERMINAL_PANE_HOOK_SOURCE_PATTERN.test(name)) @@ -84,7 +88,7 @@ function readFlattenedHookOrder(): string[] { describe('TerminalPane refactor hook parity', () => { it('preserves the recursively flattened render hook order', () => { const hooks = readFlattenedHookOrder() - expect(hooks).toHaveLength(209) + expect(hooks).toHaveLength(212) expect(hooks.filter((hook) => hook === 'useMemo')).toHaveLength(8) expect(createHash('sha256').update(hooks.join('\n')).digest('hex')).toBe( PRE_REFACTOR_HOOK_ORDER_SHA256 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) } } diff --git a/src/renderer/src/components/terminal-pane/terminal-render-desync-trigger.ts b/src/renderer/src/components/terminal-pane/terminal-render-desync-trigger.ts index e65b19030e7..fa872efde43 100644 --- a/src/renderer/src/components/terminal-pane/terminal-render-desync-trigger.ts +++ b/src/renderer/src/components/terminal-pane/terminal-render-desync-trigger.ts @@ -118,3 +118,12 @@ function removeClickListener(): void { clickListener = null } } + +if (import.meta !== undefined && import.meta.hot) { + // Vite can replace this module without a full renderer reload. Remove the + // opt-in gesture hook so dev sessions do not retain stale pane closures. + import.meta.hot.dispose(() => { + removeClickListener() + stopRenderDesyncSampleBurst() + }) +} diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts index 330b42166cd..ea85e929e81 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts @@ -1,4 +1,4 @@ -import { useCallback, useImperativeHandle } from 'react' +import { useCallback, useImperativeHandle, useRef } from 'react' import { useAppStore } from '../../store' import type { PaneExternalDropTarget } from '@/lib/pane-manager/pane-manager' import { makePaneKey } from '../../../../shared/stable-pane-id' @@ -13,8 +13,11 @@ import { } from './terminal-pane-tab-detach' import { clearPaneTerminalError } from './terminal-error-accumulation' import type { TerminalPaneBindingController } from './use-terminal-pane-layout-bindings' +import { retireUnboundIpcTerminalPane } from './retire-unbound-ipc-terminal-pane' +import { capturePendingTerminalPaneClose } from './terminal-pane-close-admission' export function useTerminalPaneCloseActions(controller: TerminalPaneBindingController) { + const confirmedCloseRef = useRef<(() => void) | null>(null) const { clearSessionRestoredBannerForPane, managerRef, @@ -46,6 +49,13 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr clearSessionRestoredBannerForPane(paneId) const leafId = manager.getLeafId(paneId) if (leafId) { + retireUnboundIpcTerminalPane({ + getState: useAppStore.getState, + tabId, + leafId, + transport: paneTransportsRef.current.get(paneId), + getTransports: () => paneTransportsRef.current + }) useAppStore.getState().setCacheTimerStartedAt(makePaneKey(tabId, leafId), null) useAppStore.getState().dropAgentStatus(makePaneKey(tabId, leafId), { paneRemoved: true }) } @@ -79,12 +89,18 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr return } const transport = paneTransportsRef.current.get(paneId) - const ptyId = transport?.getPtyId() + const pending = capturePendingTerminalPaneClose(controller, paneId, useAppStore.getState) + const ptyId = transport?.getPtyId() ?? pending?.ptyId if (!ptyId) { executeClosePane(paneId) return } const settings = useAppStore.getState().settings + const close = (): void => { + if (!pending || pending.isCurrent()) { + executeClosePane(paneId) + } + } let decided = false const decide = (act: () => void): void => { if (decided) { @@ -93,12 +109,23 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr decided = true act() } - const confirmClose = (): void => + const confirmClose = (): void => { + if (pending && !pending.isCurrent()) { + return + } + confirmedCloseRef.current = close setPendingCloseConfirmation({ paneId, copyKind: getCloseDialogCopyKind(paneId) }) - const probeTimeout = setTimeout(() => decide(confirmClose), RUNNING_CLOSE_PROBE_TIMEOUT_MS) + } + const probeTimeout = setTimeout( + () => + decide( + pending && settings?.skipCloseTerminalWithRunningProcessConfirm ? close : confirmClose + ), + RUNNING_CLOSE_PROBE_TIMEOUT_MS + ) // Why the shared probe rather than a direct inspect: this is the same question the tab-close // guard asks, and the two must not drift on what an unanswered host means. void probePtyRunningWork(settings, [ptyId], { timeoutMs: RUNNING_CLOSE_PROBE_TIMEOUT_MS }) @@ -106,10 +133,10 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr clearTimeout(probeTimeout) decide(() => { if ( - probes[0]?.verdict !== 'live' || + (pending ? probes[0]?.verdict === 'exited' : probes[0]?.verdict !== 'live') || settings?.skipCloseTerminalWithRunningProcessConfirm ) { - executeClosePane(paneId) + close() } else { confirmClose() } @@ -117,7 +144,9 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr }) .catch(() => { clearTimeout(probeTimeout) - decide(() => executeClosePane(paneId)) + decide( + pending && !settings?.skipCloseTerminalWithRunningProcessConfirm ? confirmClose : close + ) }) }, // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. @@ -143,22 +172,24 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr }, []) const handleConfirmClose = useCallback( (dontAskAgain: boolean) => { - if (pendingCloseConfirmation === null) { + if (pendingCloseConfirmation === null || confirmedCloseRef.current === null) { return } - const paneId = pendingCloseConfirmation.paneId + const confirmedClose = confirmedCloseRef.current + confirmedCloseRef.current = null setPendingCloseConfirmation(null) if (dontAskAgain) { void updateSettings({ skipCloseTerminalWithRunningProcessConfirm: true }) } - executeClosePane(paneId) + confirmedClose() }, // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. [executeClosePane, pendingCloseConfirmation, updateSettings] ) const handleCancelClose = useCallback(() => { + confirmedCloseRef.current = null setPendingCloseConfirmation(null) // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. }, []) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts b/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts index 2376f842c9a..145a26690c7 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts @@ -119,9 +119,7 @@ export function useTerminalTabColdParking(args: { // Why the worktree-scoped set, not the record map: the map is app-global, so // subscribing to it re-rendered this worktree on every other worktree's write. const sleepingRecordOwnedTabIds = useAppStore( - useShallow((state) => - selectSleepingRecordParkExemptTabIds(state.sleepingAgentSessionsByPaneKey, worktreeId) - ) + useShallow((state) => selectSleepingRecordParkExemptTabIds(state, worktreeId)) ) const terminalTabHiddenSinceRef = useRef(new Map()) // Why: view switches hide every tab at once, so the park clock cannot rank them. diff --git a/src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts b/src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts new file mode 100644 index 00000000000..0a11c271ef0 --- /dev/null +++ b/src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts @@ -0,0 +1,95 @@ +// @vitest-environment happy-dom + +import { act, renderHook } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { useBranchStartPointActions } from './branch-start-point-actions' + +type Input = Parameters[0] + +function createInput(overrides: Partial = {}): Input { + return { + applyLinkedGitLabWorkItem: vi.fn(), + applyLinkedWorkItem: vi.fn(), + baseBranch: undefined, + baseBranchNamesWorkspace: true, + branchAutoNameRef: { current: 'alice/sta-42-fix-export' }, + handleRepoChange: vi.fn(), + initialProjectGroupAppliedRef: { current: false }, + lastAutoNoteRef: { current: '' }, + noteRef: { current: '' }, + setBaseBranch: vi.fn(), + setBaseBranchNamesWorkspace: vi.fn(), + setBranchNameOverride: vi.fn(), + setBranchNameOverridePreservesNameEdits: vi.fn(), + setCompareBaseRef: vi.fn(), + setForkPushWarning: vi.fn(), + setNote: vi.fn(), + setProjectError: vi.fn(), + setSelectedProjectGroupId: vi.fn(), + setPushTarget: vi.fn(), + setReuseEligibleBranch: vi.fn(), + setReuseSelectedBranch: vi.fn(), + setSparseDirectories: vi.fn(), + setSparseEnabled: vi.fn(), + setSparseSelectedPresetId: vi.fn(), + setStartFromResetHint: vi.fn(), + smartGitHubPrStartPointSelectionRef: { current: null }, + ...overrides + } +} + +describe('composer base changes', () => { + it('marks a PR head as source-owned after replacing an independent base', () => { + const input = createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: false }) + const { result } = renderHook(() => useBranchStartPointActions(input)) + + act(() => + result.current.handleBaseBranchPrSelect('refs/pull/42/head', { + id: 'pr-42', + type: 'pr', + number: 42, + title: 'Fix export', + state: 'open', + url: 'https://github.com/o/r/pull/42', + labels: [], + updatedAt: '', + author: null, + repoId: 'repo-1' + }) + ) + + expect(input.setBaseBranch).toHaveBeenCalledWith('refs/pull/42/head') + expect(input.setBaseBranchNamesWorkspace).toHaveBeenCalledWith(true) + }) + + it.each([ + { baseBranch: undefined, baseBranchNamesWorkspace: true }, + { baseBranch: 'release/1.2', baseBranchNamesWorkspace: false } + ])('preserves independent branch naming state from %j', (previous) => { + const input = createInput(previous) + const { result } = renderHook(() => useBranchStartPointActions(input)) + + act(() => result.current.handleBaseBranchChange('release/2.0')) + act(() => result.current.handleBaseBranchChange(undefined)) + + expect(input.setBaseBranch).toHaveBeenNthCalledWith(1, 'release/2.0') + expect(input.setBaseBranch).toHaveBeenNthCalledWith(2, undefined) + expect(input.setBaseBranchNamesWorkspace).toHaveBeenCalledWith(false) + expect(input.setBranchNameOverride).not.toHaveBeenCalled() + expect(input.setBranchNameOverridePreservesNameEdits).not.toHaveBeenCalled() + expect(input.branchAutoNameRef.current).toBe('alice/sta-42-fix-export') + }) + + it('still drops branch reuse and its pinned name when replacing a source-owned base', () => { + const input = createInput({ baseBranch: 'feature/existing' }) + const { result } = renderHook(() => useBranchStartPointActions(input)) + + act(() => result.current.handleBaseBranchChange('release/2.0')) + + expect(input.setBranchNameOverride).toHaveBeenCalledWith(undefined) + expect(input.setBranchNameOverridePreservesNameEdits).toHaveBeenCalledWith(false) + expect(input.setReuseEligibleBranch).toHaveBeenCalledWith(null) + expect(input.setReuseSelectedBranch).toHaveBeenCalledWith(false) + expect(input.branchAutoNameRef.current).toBe('') + }) +}) diff --git a/src/renderer/src/hooks/composer-state/branch-start-point-actions.ts b/src/renderer/src/hooks/composer-state/branch-start-point-actions.ts index 304ba065bbd..6a5a7b0af8c 100644 --- a/src/renderer/src/hooks/composer-state/branch-start-point-actions.ts +++ b/src/renderer/src/hooks/composer-state/branch-start-point-actions.ts @@ -4,12 +4,15 @@ type BranchStartPointActionsInput = Pick< ComposerModel, | 'applyLinkedGitLabWorkItem' | 'applyLinkedWorkItem' + | 'baseBranch' + | 'baseBranchNamesWorkspace' | 'branchAutoNameRef' | 'handleRepoChange' | 'initialProjectGroupAppliedRef' | 'lastAutoNoteRef' | 'noteRef' | 'setBaseBranch' + | 'setBaseBranchNamesWorkspace' | 'setBranchNameOverride' | 'setBranchNameOverridePreservesNameEdits' | 'setCompareBaseRef' @@ -38,12 +41,15 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) const { applyLinkedGitLabWorkItem, applyLinkedWorkItem, + baseBranch, + baseBranchNamesWorkspace, branchAutoNameRef, handleRepoChange, initialProjectGroupAppliedRef, lastAutoNoteRef, noteRef, setBaseBranch, + setBaseBranchNamesWorkspace, setBranchNameOverride, setBranchNameOverridePreservesNameEdits, setCompareBaseRef, @@ -91,20 +97,26 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) (next: string | undefined): void => { smartGitHubPrStartPointSelectionRef.current = null setBaseBranch(next) + setBaseBranchNamesWorkspace(false) setCompareBaseRef(undefined) setPushTarget(undefined) - setBranchNameOverride(undefined) - // Why (#5181): Start-from means "new branch from this base", so it never reuses — clear reuse state from a prior smart-field branch pick. - setBranchNameOverridePreservesNameEdits(false) + // Only source-owned refs pin a name that changing the base invalidates. + if (baseBranch && baseBranchNamesWorkspace) { + setBranchNameOverride(undefined) + setBranchNameOverridePreservesNameEdits(false) + branchAutoNameRef.current = '' + } setReuseEligibleBranch(null) setReuseSelectedBranch(false) setForkPushWarning(null) - branchAutoNameRef.current = '' setStartFromResetHint(null) }, [ + baseBranch, + baseBranchNamesWorkspace, branchAutoNameRef, setBaseBranch, + setBaseBranchNamesWorkspace, setBranchNameOverride, setBranchNameOverridePreservesNameEdits, setCompareBaseRef, @@ -126,6 +138,7 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) nextCompareBaseRef?: string ): void => { setBaseBranch(nextBaseBranch) + setBaseBranchNamesWorkspace(true) setCompareBaseRef(nextCompareBaseRef) setPushTarget(nextPushTarget) setBranchNameOverride(nextBranchNameOverride) @@ -151,6 +164,7 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) lastAutoNoteRef, noteRef, setBaseBranch, + setBaseBranchNamesWorkspace, setBranchNameOverride, setBranchNameOverridePreservesNameEdits, setCompareBaseRef, @@ -169,6 +183,7 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) nextCompareBaseRef?: string ): void => { setBaseBranch(nextBaseBranch) + setBaseBranchNamesWorkspace(true) setCompareBaseRef(nextCompareBaseRef) setPushTarget(nextPushTarget) setBranchNameOverride(undefined) @@ -190,6 +205,7 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) lastAutoNoteRef, noteRef, setBaseBranch, + setBaseBranchNamesWorkspace, setBranchNameOverride, setCompareBaseRef, setNote, diff --git a/src/renderer/src/hooks/composer-state/composer-external-sync.ts b/src/renderer/src/hooks/composer-state/composer-external-sync.ts index c572f5b37b5..d3dba81665a 100644 --- a/src/renderer/src/hooks/composer-state/composer-external-sync.ts +++ b/src/renderer/src/hooks/composer-state/composer-external-sync.ts @@ -85,6 +85,7 @@ export function useComposerExternalSync(target: ComposerTargetState): ComposerEx selectedRepoGitHubSourceContext: target.sourceContextState.selectedRepoGitHubSourceContext, selectedRepoIsGit: target.runtimeTargetSelection.selectedRepoIsGit, setBaseBranch: target.workspaceIdentityState.setBaseBranch, + setBaseBranchNamesWorkspace: target.workspaceIdentityState.setBaseBranchNamesWorkspace, setBranchNameOverride: target.workspaceIdentityState.setBranchNameOverride, setBranchNameOverridePreservesNameEdits: target.workspaceIdentityState.setBranchNameOverridePreservesNameEdits, diff --git a/src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts b/src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts new file mode 100644 index 00000000000..3de3c57099d --- /dev/null +++ b/src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts @@ -0,0 +1,193 @@ +// @vitest-environment happy-dom + +import { act, renderHook } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { useIssueSourceActions } from './issue-source-actions' +import { resolveDraftBaseBranchNamesWorkspace } from './workspace-identity-state' +import { useGitHubProviderSelection } from './github-provider-selection' + +type Input = Parameters[0] + +function createInput(overrides: Partial = {}): Input { + return { + baseBranch: undefined, + baseBranchNamesWorkspace: true, + branchAutoNameRef: { current: '' }, + isProjectGroupTarget: false, + lastAutoNameRef: { current: '' }, + lastAutoNoteRef: { current: '' }, + linkedWorkItem: null, + name: '', + noteRef: { current: '' }, + setBaseBranch: vi.fn(), + setBranchNameOverride: vi.fn(), + setBranchNameOverridePreservesNameEdits: vi.fn(), + setCompareBaseRef: vi.fn(), + setForkPushWarning: vi.fn(), + setLinkedGitLabIssue: vi.fn(), + setLinkedGitLabMR: vi.fn(), + setLinkedIssue: vi.fn(), + setLinkedPR: vi.fn(), + setLinkedTaskSourceContext: vi.fn(), + setLinkedWorkItem: vi.fn(), + setName: vi.fn(), + setNote: vi.fn(), + setPushTarget: vi.fn(), + setReuseEligibleBranch: vi.fn(), + setReuseSelectedBranch: vi.fn(), + setStartFromResetHint: vi.fn(), + smartGitHubPrStartPointSelectionRef: { current: null }, + ...overrides + } as Input +} + +describe('composer name source selection', () => { + it.each([false, true])('clears only source-owned bases when selecting Jira (%s)', (owned) => { + const input = createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: owned }) + const { result } = renderHook(() => useIssueSourceActions(input)) + + act(() => + result.current.handleSmartJiraIssueSelect( + { + id: '42', + key: 'APP-42', + title: 'Fix export', + url: 'https://jira.example/browse/APP-42', + project: { id: 'app', key: 'APP', name: 'App' }, + issueType: { id: 'bug', name: 'Bug' }, + status: { id: 'open', name: 'Open', categoryKey: 'new', categoryName: 'To do' }, + labels: [], + updatedAt: '', + createdAt: '' + }, + { kind: 'task-source', provider: 'jira', projectId: 'app', hostId: 'local' } + ) + ) + + expect(input.setBaseBranch).toHaveBeenCalledTimes(owned ? 1 : 0) + if (owned) { + expect(input.setBaseBranch).toHaveBeenCalledWith(undefined) + } + }) + + it.each([false, true])('clears only source-owned bases when removing a source (%s)', (owned) => { + const input = createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: owned }) + const { result } = renderHook(() => useIssueSourceActions(input)) + + act(() => result.current.handleClearSmartNameSelection()) + + expect(input.setBaseBranch).toHaveBeenCalledTimes(owned ? 1 : 0) + if (owned) { + expect(input.setBaseBranch).toHaveBeenCalledWith(undefined) + } + }) + + it.each([false, true])( + 'clears only source-owned bases when selecting a GitHub issue (%s)', + (owned) => { + const input = { + ...createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: owned }), + applyLinkedWorkItem: vi.fn(), + eligibleRepos: [], + handleBaseBranchPrSelect: vi.fn(), + selectedRepo: undefined, + selectedRepoGitHubSourceContext: null, + settings: null + } + const { result } = renderHook(() => useGitHubProviderSelection(input)) + + act(() => + result.current.handleSmartGitHubItemSelect({ + id: 'issue-42', + type: 'issue', + number: 42, + title: 'Fix export', + state: 'open', + url: 'https://github.com/o/r/issues/42', + labels: [], + updatedAt: '', + author: null, + repoId: 'repo-1' + }) + ) + + expect(input.setBaseBranch).toHaveBeenCalledTimes(owned ? 1 : 0) + if (owned) { + expect(input.setBaseBranch).toHaveBeenCalledWith(undefined) + } + } + ) + + it('names the workspace after a branch the name field picked', () => { + const { result } = renderHook(() => + useIssueSourceActions( + createInput({ baseBranch: 'feature/export-v2', baseBranchNamesWorkspace: true }) + ) + ) + + expect(result.current.smartNameSelection).toEqual({ + kind: 'branch', + label: 'feature/export-v2' + }) + }) + + // Why: the field swaps the text input for a source pill, so a pill here would hide a typed name. + it('claims no source for a base ref the composer picker chose, leaving a typed name on screen', () => { + const { result } = renderHook(() => + useIssueSourceActions( + createInput({ + baseBranch: 'release/1.2', + baseBranchNamesWorkspace: false, + name: 'my-own-name' + }) + ) + ) + + expect(result.current.smartNameSelection).toBeNull() + }) + + it('keeps a linked issue as the source whatever the base ref', () => { + const { result } = renderHook(() => + useIssueSourceActions( + createInput({ + baseBranch: 'release/1.2', + baseBranchNamesWorkspace: false, + linkedWorkItem: { + type: 'issue', + number: 42, + title: 'Broken export', + url: 'https://github.com/o/r/issues/42' + } as Input['linkedWorkItem'] + }) + ) + ) + + expect(result.current.smartNameSelection?.kind).toBe('github-issue') + }) +}) + +describe('base ref intent across a draft round trip', () => { + it('restores a base ref chosen in the picker as a base, not as a name source', () => { + expect(resolveDraftBaseBranchNamesWorkspace({ persistDraft: true, draftValue: false })).toBe( + false + ) + }) + + it('restores a branch picked in the name field as a name source', () => { + expect(resolveDraftBaseBranchNamesWorkspace({ persistDraft: true, draftValue: true })).toBe( + true + ) + }) + + it('treats a draft written before the flag existed as a name source', () => { + expect( + resolveDraftBaseBranchNamesWorkspace({ persistDraft: true, draftValue: undefined }) + ).toBe(true) + }) + + it('ignores any stored intent when the composer does not persist drafts', () => { + expect(resolveDraftBaseBranchNamesWorkspace({ persistDraft: false, draftValue: false })).toBe( + true + ) + }) +}) diff --git a/src/renderer/src/hooks/composer-state/composer-source-state.ts b/src/renderer/src/hooks/composer-state/composer-source-state.ts index 8b1a8dbe566..b19e6ee75b8 100644 --- a/src/renderer/src/hooks/composer-state/composer-source-state.ts +++ b/src/renderer/src/hooks/composer-state/composer-source-state.ts @@ -132,6 +132,8 @@ export function useComposerSourceState( workspaceHostScope: target.composerTargetStore.workspaceHostScope }) const branchStartPointActions = useBranchStartPointActions({ + baseBranch: target.workspaceIdentityState.baseBranch, + baseBranchNamesWorkspace: target.workspaceIdentityState.baseBranchNamesWorkspace, applyLinkedGitLabWorkItem: sourceIdentityActions.applyLinkedGitLabWorkItem, applyLinkedWorkItem: external.githubSourceApplication.applyLinkedWorkItem, branchAutoNameRef: target.asyncComposerState.branchAutoNameRef, @@ -140,6 +142,7 @@ export function useComposerSourceState( lastAutoNoteRef: target.asyncComposerState.lastAutoNoteRef, noteRef: target.asyncComposerState.noteRef, setBaseBranch: target.workspaceIdentityState.setBaseBranch, + setBaseBranchNamesWorkspace: target.workspaceIdentityState.setBaseBranchNamesWorkspace, setBranchNameOverride: target.workspaceIdentityState.setBranchNameOverride, setBranchNameOverridePreservesNameEdits: target.workspaceIdentityState.setBranchNameOverridePreservesNameEdits, @@ -159,6 +162,7 @@ export function useComposerSourceState( target.asyncComposerState.smartGitHubPrStartPointSelectionRef }) const githubProviderSelection = useGitHubProviderSelection({ + baseBranchNamesWorkspace: target.workspaceIdentityState.baseBranchNamesWorkspace, applyLinkedWorkItem: external.githubSourceApplication.applyLinkedWorkItem, branchAutoNameRef: target.asyncComposerState.branchAutoNameRef, eligibleRepos: target.composerTargetStore.eligibleRepos, @@ -220,6 +224,7 @@ export function useComposerSourceState( repoId: target.initialTargetState.repoId, reuseEligibleBranch: target.workspaceIdentityState.reuseEligibleBranch, setBaseBranch: target.workspaceIdentityState.setBaseBranch, + setBaseBranchNamesWorkspace: target.workspaceIdentityState.setBaseBranchNamesWorkspace, setBranchNameOverride: target.workspaceIdentityState.setBranchNameOverride, setBranchNameOverridePreservesNameEdits: target.workspaceIdentityState.setBranchNameOverridePreservesNameEdits, @@ -236,6 +241,7 @@ export function useComposerSourceState( }) const issueSourceActions = useIssueSourceActions({ baseBranch: target.workspaceIdentityState.baseBranch, + baseBranchNamesWorkspace: target.workspaceIdentityState.baseBranchNamesWorkspace, branchAutoNameRef: target.asyncComposerState.branchAutoNameRef, isProjectGroupTarget: target.runtimeTargetSelection.isProjectGroupTarget, lastAutoNameRef: target.asyncComposerState.lastAutoNameRef, diff --git a/src/renderer/src/hooks/composer-state/composer-target-state.ts b/src/renderer/src/hooks/composer-state/composer-target-state.ts index bb73549cac0..bbc3eb2fe47 100644 --- a/src/renderer/src/hooks/composer-state/composer-target-state.ts +++ b/src/renderer/src/hooks/composer-state/composer-target-state.ts @@ -156,6 +156,7 @@ export function useComposerTargetState( agentPrompt: sourceContextState.agentPrompt, attachmentPaths: sourceContextState.attachmentPaths, baseBranch: workspaceIdentityState.baseBranch, + baseBranchNamesWorkspace: workspaceIdentityState.baseBranchNamesWorkspace, compareBaseRef: workspaceIdentityState.compareBaseRef, eligibleRepos: composerTargetStore.eligibleRepos, fetchSparsePresets: composerTargetStore.fetchSparsePresets, diff --git a/src/renderer/src/hooks/composer-state/draft-target-sync.test.ts b/src/renderer/src/hooks/composer-state/draft-target-sync.test.ts index a44847bd5b2..1f74d15e699 100644 --- a/src/renderer/src/hooks/composer-state/draft-target-sync.test.ts +++ b/src/renderer/src/hooks/composer-state/draft-target-sync.test.ts @@ -19,6 +19,7 @@ function createState(overrides: Partial = {}): DraftTarget agentPrompt: 'Fix the issue', attachmentPaths: ['/tmp/context.txt'], baseBranch: 'main', + baseBranchNamesWorkspace: true, compareBaseRef: 'origin/main', eligibleRepos: [], fetchSparsePresets: vi.fn(), @@ -78,6 +79,25 @@ describe('useDraftTargetSync', () => { expect(setRepoId).toHaveBeenCalledWith('repo-1') }) + it('persists whether the base ref also names the workspace', () => { + const setNewWorkspaceDraft = vi.fn() + const state = createState({ + eligibleRepos: [createRepo('repo-1')], + baseBranch: 'release/2.1', + baseBranchNamesWorkspace: false, + setNewWorkspaceDraft + }) + + renderHook(() => useDraftTargetSync(state)) + + expect(setNewWorkspaceDraft).toHaveBeenCalledWith( + expect.objectContaining({ + baseBranch: 'release/2.1', + baseBranchNamesWorkspace: false + }) + ) + }) + it('does not persist or repair transient quick-composer state', () => { const state = createState({ persistDraft: false, diff --git a/src/renderer/src/hooks/composer-state/draft-target-sync.ts b/src/renderer/src/hooks/composer-state/draft-target-sync.ts index 8783f0ef0d0..dcfc90b53bc 100644 --- a/src/renderer/src/hooks/composer-state/draft-target-sync.ts +++ b/src/renderer/src/hooks/composer-state/draft-target-sync.ts @@ -6,6 +6,7 @@ export type DraftTargetSyncInput = Pick< | 'agentPrompt' | 'attachmentPaths' | 'baseBranch' + | 'baseBranchNamesWorkspace' | 'compareBaseRef' | 'eligibleRepos' | 'fetchSparsePresets' @@ -36,6 +37,7 @@ export function useDraftTargetSync(state: DraftTargetSyncInput): void { agentPrompt, attachmentPaths, baseBranch, + baseBranchNamesWorkspace, compareBaseRef, eligibleRepos, fetchSparsePresets, @@ -98,7 +100,7 @@ export function useDraftTargetSync(state: DraftTargetSyncInput): void { linkedPR, linkedGitLabIssue, linkedGitLabMR, - ...(baseBranch !== undefined ? { baseBranch } : {}), + ...(baseBranch !== undefined ? { baseBranch, baseBranchNamesWorkspace } : {}), ...(compareBaseRef !== undefined ? { compareBaseRef } : {}) }) }, [ @@ -106,6 +108,7 @@ export function useDraftTargetSync(state: DraftTargetSyncInput): void { agentPrompt, attachmentPaths, baseBranch, + baseBranchNamesWorkspace, compareBaseRef, linkedIssue, linkedPR, diff --git a/src/renderer/src/hooks/composer-state/github-provider-selection.ts b/src/renderer/src/hooks/composer-state/github-provider-selection.ts index e124abefe96..cfa5ebbd3cd 100644 --- a/src/renderer/src/hooks/composer-state/github-provider-selection.ts +++ b/src/renderer/src/hooks/composer-state/github-provider-selection.ts @@ -3,6 +3,7 @@ import type { ComposerModel } from './composer-model' type GitHubProviderSelectionInput = Pick< ComposerModel, | 'applyLinkedWorkItem' + | 'baseBranchNamesWorkspace' | 'branchAutoNameRef' | 'eligibleRepos' | 'handleBaseBranchPrSelect' @@ -47,6 +48,7 @@ import type { SmartGitHubPrStartPointSelection } from './source-selection-decisi export function useGitHubProviderSelection(input: GitHubProviderSelectionInput) { const { applyLinkedWorkItem, + baseBranchNamesWorkspace, branchAutoNameRef, eligibleRepos, handleBaseBranchPrSelect, @@ -112,7 +114,9 @@ export function useGitHubProviderSelection(input: GitHubProviderSelectionInput) const runRepo = selectedRepo ?? eligibleRepos.find((repo) => repo.id === item.repoId) applyLinkedWorkItem(normalizedItem) if (identity.type !== 'pr' || !runRepo) { - setBaseBranch(undefined) + if (identity.type === 'pr' || baseBranchNamesWorkspace) { + setBaseBranch(undefined) + } setCompareBaseRef(undefined) setPushTarget(undefined) return @@ -171,6 +175,7 @@ export function useGitHubProviderSelection(input: GitHubProviderSelectionInput) }, [ applyLinkedWorkItem, + baseBranchNamesWorkspace, eligibleRepos, handleBaseBranchPrSelect, isProjectGroupTarget, diff --git a/src/renderer/src/hooks/composer-state/github-submit-resolution.ts b/src/renderer/src/hooks/composer-state/github-submit-resolution.ts index 3e23e3492e6..bab2db999b3 100644 --- a/src/renderer/src/hooks/composer-state/github-submit-resolution.ts +++ b/src/renderer/src/hooks/composer-state/github-submit-resolution.ts @@ -12,6 +12,7 @@ type GitHubSubmitResolutionInput = Pick< | 'selectedRepoGitHubSourceContext' | 'selectedRepoIsGit' | 'setBaseBranch' + | 'setBaseBranchNamesWorkspace' | 'setBranchNameOverride' | 'setBranchNameOverridePreservesNameEdits' | 'setCompareBaseRef' @@ -62,6 +63,7 @@ export function useGitHubSubmitResolution(input: GitHubSubmitResolutionInput) { selectedRepoGitHubSourceContext, selectedRepoIsGit, setBaseBranch, + setBaseBranchNamesWorkspace, setBranchNameOverride, setBranchNameOverridePreservesNameEdits, setCompareBaseRef, @@ -133,6 +135,7 @@ export function useGitHubSubmitResolution(input: GitHubSubmitResolutionInput) { : {}) } setBaseBranch(selectedPrStartPoint.baseBranch) + setBaseBranchNamesWorkspace(true) setCompareBaseRef(selectedPrStartPoint.compareBaseRef) setPushTarget(selectedPrStartPoint.pushTarget) if (selectedPrStartPoint.branchNameOverride) { @@ -234,6 +237,7 @@ export function useGitHubSubmitResolution(input: GitHubSubmitResolutionInput) { lastAutoNameRef.current = resolution.workspaceName if (prStartPoint) { setBaseBranch(prStartPoint.baseBranch) + setBaseBranchNamesWorkspace(true) setCompareBaseRef(prStartPoint.compareBaseRef) setPushTarget(prStartPoint.pushTarget) if (prStartPoint.branchNameOverride) { @@ -263,6 +267,7 @@ export function useGitHubSubmitResolution(input: GitHubSubmitResolutionInput) { branchAutoNameRef, lastAutoNameRef, setBaseBranch, + setBaseBranchNamesWorkspace, setBranchNameOverride, setBranchNameOverridePreservesNameEdits, setCompareBaseRef, diff --git a/src/renderer/src/hooks/composer-state/identity-model.ts b/src/renderer/src/hooks/composer-state/identity-model.ts index e375dd754d7..b207d3ed88b 100644 --- a/src/renderer/src/hooks/composer-state/identity-model.ts +++ b/src/renderer/src/hooks/composer-state/identity-model.ts @@ -14,6 +14,8 @@ export type ComposerIdentityModel = { setLinkedGitLabMR: React.Dispatch> baseBranch: string | undefined setBaseBranch: React.Dispatch> + baseBranchNamesWorkspace: boolean + setBaseBranchNamesWorkspace: React.Dispatch> compareBaseRef: string | undefined setCompareBaseRef: React.Dispatch> branchNameOverride: string | undefined diff --git a/src/renderer/src/hooks/composer-state/issue-source-actions.ts b/src/renderer/src/hooks/composer-state/issue-source-actions.ts index 049e40fdf7b..64a6cf54e9a 100644 --- a/src/renderer/src/hooks/composer-state/issue-source-actions.ts +++ b/src/renderer/src/hooks/composer-state/issue-source-actions.ts @@ -3,6 +3,7 @@ import type { ComposerModel } from './composer-model' type IssueSourceActionsInput = Pick< ComposerModel, | 'baseBranch' + | 'baseBranchNamesWorkspace' | 'branchAutoNameRef' | 'isProjectGroupTarget' | 'lastAutoNameRef' @@ -59,6 +60,7 @@ import type { TaskSourceContext } from '../../../../shared/task-source-context' export function useIssueSourceActions(input: IssueSourceActionsInput) { const { baseBranch, + baseBranchNamesWorkspace, branchAutoNameRef, isProjectGroupTarget, lastAutoNameRef, @@ -161,7 +163,9 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { setLinkedPR(null) setLinkedGitLabIssue(null) setLinkedGitLabMR(null) - setBaseBranch(undefined) + if (baseBranchNamesWorkspace) { + setBaseBranch(undefined) + } setCompareBaseRef(undefined) setPushTarget(undefined) setBranchNameOverride(undefined) @@ -187,6 +191,7 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { }, [ name, + baseBranchNamesWorkspace, branchAutoNameRef, lastAutoNameRef, setBaseBranch, @@ -213,7 +218,9 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { setLinkedGitLabMR(null) setLinkedWorkItem(null) setLinkedTaskSourceContext(null) - setBaseBranch(undefined) + if (baseBranchNamesWorkspace) { + setBaseBranch(undefined) + } setCompareBaseRef(undefined) setPushTarget(undefined) setBranchNameOverride(undefined) @@ -233,6 +240,7 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { } }, [ name, + baseBranchNamesWorkspace, branchAutoNameRef, lastAutoNameRef, lastAutoNoteRef, @@ -263,9 +271,11 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { } return buildWorkspaceSourceSelection({ linkedWorkItem, - baseBranch + // Why: only a branch picked to NAME the workspace becomes a source pill; a base ref + // chosen in the composer's own picker must leave a typed name on screen. + baseBranch: baseBranchNamesWorkspace ? baseBranch : undefined }) as SmartWorkspaceNameSelection | null - }, [baseBranch, isProjectGroupTarget, linkedWorkItem]) + }, [baseBranch, baseBranchNamesWorkspace, isProjectGroupTarget, linkedWorkItem]) return { handleSmartLinearIssueSelect, diff --git a/src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts b/src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts index 910143cace8..c2ba2f2664d 100644 --- a/src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts +++ b/src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts @@ -63,6 +63,8 @@ function useSelectedSourceReset( ) const source = useIssueSourceActions({ baseBranch, + // Why: a PR/issue/branch source owns the base, so "create more" must still clear it. + baseBranchNamesWorkspace: true, branchAutoNameRef, isProjectGroupTarget, lastAutoNameRef, diff --git a/src/renderer/src/hooks/composer-state/work-item-source-actions.ts b/src/renderer/src/hooks/composer-state/work-item-source-actions.ts index 409b34cb729..75763f9c772 100644 --- a/src/renderer/src/hooks/composer-state/work-item-source-actions.ts +++ b/src/renderer/src/hooks/composer-state/work-item-source-actions.ts @@ -8,6 +8,7 @@ type WorkItemSourceActionsInput = Pick< | 'repoId' | 'reuseEligibleBranch' | 'setBaseBranch' + | 'setBaseBranchNamesWorkspace' | 'setBranchNameOverride' | 'setBranchNameOverridePreservesNameEdits' | 'setCompareBaseRef' @@ -35,6 +36,7 @@ export function useWorkItemSourceActions(input: WorkItemSourceActionsInput) { repoId, reuseEligibleBranch, setBaseBranch, + setBaseBranchNamesWorkspace, setBranchNameOverride, setBranchNameOverridePreservesNameEdits, setCompareBaseRef, @@ -59,6 +61,7 @@ export function useWorkItemSourceActions(input: WorkItemSourceActionsInput) { worktreeBranches: getComposerRepoWorktreeBranches(worktreesByRepo[repoId] ?? [], repoId) }) setBaseBranch(selection.baseBranch) + setBaseBranchNamesWorkspace(true) setCompareBaseRef(undefined) setPushTarget(undefined) setStartFromResetHint(null) @@ -86,6 +89,7 @@ export function useWorkItemSourceActions(input: WorkItemSourceActionsInput) { branchAutoNameRef, lastAutoNameRef, setBaseBranch, + setBaseBranchNamesWorkspace, setBranchNameOverride, setBranchNameOverridePreservesNameEdits, setCompareBaseRef, diff --git a/src/renderer/src/hooks/composer-state/workspace-identity-state.ts b/src/renderer/src/hooks/composer-state/workspace-identity-state.ts index 6b7e73e9417..b31878ccb96 100644 --- a/src/renderer/src/hooks/composer-state/workspace-identity-state.ts +++ b/src/renderer/src/hooks/composer-state/workspace-identity-state.ts @@ -27,6 +27,19 @@ import { filterEnabledTuiAgents, isTuiAgentEnabled } from '../../../../shared/tu import { getAgentCatalog } from '@/lib/agent-catalog' import { useAppStore } from '@/store' +/** + * Whether a restored draft's `baseBranch` still names the workspace. + * + * A draft written before the flag existed carries no intent, so it restores as a branch pick — + * the behavior it had when it was saved. + */ +export function resolveDraftBaseBranchNamesWorkspace(args: { + persistDraft: boolean + draftValue: boolean | undefined +}): boolean { + return args.persistDraft ? (args.draftValue ?? true) : true +} + export function useWorkspaceIdentityState(input: WorkspaceIdentityStateInput) { const { initialBaseBranch, @@ -91,6 +104,14 @@ export function useWorkspaceIdentityState(input: WorkspaceIdentityStateInput) { const [baseBranch, setBaseBranch] = useState( persistDraft ? newWorkspaceDraft?.baseBranch : initialBaseBranch ) + // Why: a branch picked to name the workspace shows as a source pill; a base ref chosen in + // the composer's own picker must not, or it would hide the name the user typed. + const [baseBranchNamesWorkspace, setBaseBranchNamesWorkspace] = useState(() => + resolveDraftBaseBranchNamesWorkspace({ + persistDraft, + draftValue: newWorkspaceDraft?.baseBranchNamesWorkspace + }) + ) const [compareBaseRef, setCompareBaseRef] = useState( persistDraft ? newWorkspaceDraft?.compareBaseRef : undefined @@ -193,6 +214,8 @@ export function useWorkspaceIdentityState(input: WorkspaceIdentityStateInput) { setLinkedGitLabMR, baseBranch, setBaseBranch, + baseBranchNamesWorkspace, + setBaseBranchNamesWorkspace, compareBaseRef, setCompareBaseRef, branchNameOverride, 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 709597aec8f..c4b71668b53 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" } }, @@ -118,6 +118,9 @@ "richMarkdown": { "tooLarge": "File is larger than the {{limit}} rich editing limit. Showing source mode instead.", "openAnyway": "Open anyway" + }, + "fileLoad": { + "hostUnresolved": "The host couldn't find this file's workspace. It may have been removed, or the host may not know about it yet. Retry, or close this tab from the tab strip." } }, "githubChecks": { @@ -6356,7 +6359,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": { @@ -6545,6 +6553,11 @@ "codexConfigSyncBlankSource": "Codex is still using the settings it last synced because {{value0}} is empty. That is expected while a synced folder finishes downloading.", "codexConfigSyncManagedHomeUnavailable": "Orca could not read this account’s Codex files just now, so settings may not be syncing. This usually clears on its own — antivirus or a backup tool briefly locks them.", "codexConfigSyncUnreadableSource": "Codex is still using the settings it last synced because {{value0}} could not be read. Check that file's permissions.", + "codexLoginLinkPending": "Codex opened this sign-in link in your browser. Copy it to finish signing in somewhere else — a private window, or another profile.", + "codexLoginLinkCopied": "Copied", + "codexLoginLinkCopy": "Copy link", + "codexLoginLinkOpen": "Open", + "codexLoginLinkCopyFailed": "Could not copy the link.", "d6f1b9b6a2": "MiniMax API key is required.", "7c5d8a4e1b": "MiniMax API key was not saved.", "4d2c7b9e83": "MiniMax API key saved.", diff --git a/src/renderer/src/lib/agent-launch-route-input.ts b/src/renderer/src/lib/agent-launch-route-input.ts index 85fbf2e8ca3..2f0c1e083f4 100644 --- a/src/renderer/src/lib/agent-launch-route-input.ts +++ b/src/renderer/src/lib/agent-launch-route-input.ts @@ -1,11 +1,10 @@ -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { LOCAL_EXECUTION_HOST_ID, parseExecutionHostId, toRuntimeExecutionHostId } from '../../../shared/execution-host' import type { TuiAgent } from '../../../shared/tui-agent' -import { parseWorkspaceKey } from '../../../shared/workspace-scope' +import { workspaceKindForWorktreeId } from '../../../shared/workspace-launch-kind' import { hasExplicitTuiLaunchCommand, type AgentLaunchRoutingInput @@ -57,12 +56,7 @@ export type AgentLaunchRouteArgs = { initialSessionOptions?: Readonly> } -export function workspaceKindForWorktreeId(worktreeId: string): ProspectiveWorkspaceKind { - if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) { - return 'floating' - } - return parseWorkspaceKey(worktreeId)?.type === 'folder' ? 'folder' : 'git-worktree' -} +export { workspaceKindForWorktreeId } function resolveExecutionHostId(store: AgentLaunchRouteStore, workspace: ProspectiveWorkspace) { if (workspace.worktreeId) { diff --git a/src/renderer/src/lib/agent-launch-routing.ts b/src/renderer/src/lib/agent-launch-routing.ts index ba614053131..67d47cf1d23 100644 --- a/src/renderer/src/lib/agent-launch-routing.ts +++ b/src/renderer/src/lib/agent-launch-routing.ts @@ -5,6 +5,7 @@ import { resolveStructuredNativeChatSupport } from '../../../shared/structured-native-chat-launch-route' import type { TuiAgent } from '../../../shared/tui-agent' +import type { WorkspaceLaunchKind } from '../../../shared/workspace-launch-kind' import { decideInitialAgentTabViewMode, type NativeChatLaunchPromptDelivery @@ -28,7 +29,7 @@ export type AgentLaunchRoutingInput = { executionHostId: string /** Capabilities of the target host; `null` = not yet established. */ hostCapabilities: readonly string[] | null - workspaceKind?: 'git-worktree' | 'folder' | 'floating' + workspaceKind?: WorkspaceLaunchKind projectRuntime?: ProjectExecutionRuntimeResolution | null promptDelivery?: NativeChatLaunchPromptDelivery launchText?: string diff --git a/src/renderer/src/lib/ai-vault-session-drag.test.ts b/src/renderer/src/lib/ai-vault-session-drag.test.ts index f8af5c5a84f..ed0326eb38b 100644 --- a/src/renderer/src/lib/ai-vault-session-drag.test.ts +++ b/src/renderer/src/lib/ai-vault-session-drag.test.ts @@ -91,6 +91,33 @@ describe('Session History session drag data', () => { expect(read && 'sessionCwd' in read).toBe(true) }) + it('round-trips a structured session without a legacy resume command', () => { + const transfer = createTransfer() + const payload: AiVaultSessionDragPayload = { + agent: 'codex', + sessionId: 'session-structured', + structuredSession: { sessionId: 'session-structured', workspaceId: 'worktree-1' }, + title: 'Native chat', + command: '' + } + + writeAiVaultSessionDragData(transfer, payload) + + expect(readAiVaultSessionDragData(transfer)).toEqual(payload) + }) + + it('still rejects a blank resume command for an ordinary CLI session', () => { + const transfer = createTransfer() + writeAiVaultSessionDragData(transfer, { + agent: 'codex', + sessionId: 'session-cli', + title: 'CLI session', + command: '' + }) + + expect(readAiVaultSessionDragData(transfer)).toBeNull() + }) + it('keeps sessionCwd absent when an older serializer omitted it', () => { const transfer = createTransfer() transfer.setData( diff --git a/src/renderer/src/lib/ai-vault-session-drag.ts b/src/renderer/src/lib/ai-vault-session-drag.ts index 536616fdf81..af21ab48618 100644 --- a/src/renderer/src/lib/ai-vault-session-drag.ts +++ b/src/renderer/src/lib/ai-vault-session-drag.ts @@ -100,7 +100,9 @@ function isSerializedPayload(value: unknown): value is SerializedAiVaultSessionD isNonEmptyString(payload.sessionId) && (payload.structuredSession === undefined || isStructuredSession(payload.structuredSession)) && isNonEmptyString(payload.title) && - isNonEmptyString(payload.command) && + (payload.structuredSession + ? typeof payload.command === 'string' + : isNonEmptyString(payload.command)) && (payload.sessionFilePath === undefined || isNonEmptyString(payload.sessionFilePath)) && (payload.sessionExecutionHostId === undefined || Boolean(normalizeExecutionHostId(payload.sessionExecutionHostId))) && diff --git a/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts b/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts index 1a26b8b80b7..53842d9b749 100644 --- a/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts +++ b/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts @@ -43,6 +43,18 @@ describe('prepareAiVaultSessionForResume', () => { expect(prepareSessionResume).not.toHaveBeenCalled() }) + it('does not prepare a legacy CLI resume for a native structured session', async () => { + const prepareSessionResume = vi.fn() + stubPreparation(prepareSessionResume) + const native = session({ + codexHome: '/tmp/orca/codex-runtime-home/home', + structuredSession: { sessionId: 'session-1', workspaceId: 'worktree-1' } + }) + + await expect(prepareAiVaultSessionForResume(native)).resolves.toBe(native) + expect(prepareSessionResume).not.toHaveBeenCalled() + }) + it('repins a per-account session to the home the host substitutes', async () => { const prepareSessionResume = vi.fn().mockResolvedValue({ useRealCodexHome: false, diff --git a/src/renderer/src/lib/ai-vault-session-resume-preparation.ts b/src/renderer/src/lib/ai-vault-session-resume-preparation.ts index 9c7fa8740ff..da2514dd38a 100644 --- a/src/renderer/src/lib/ai-vault-session-resume-preparation.ts +++ b/src/renderer/src/lib/ai-vault-session-resume-preparation.ts @@ -8,7 +8,7 @@ import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' export async function prepareAiVaultSessionForResume( session: AiVaultSession ): Promise { - if (!session.structuredSession && !aiVaultSessionNeedsResumePreparation(session)) { + if (session.structuredSession || !aiVaultSessionNeedsResumePreparation(session)) { return session } const result = await window.api.aiVault.prepareSessionResume({ diff --git a/src/renderer/src/lib/input-quiet-scheduler.ts b/src/renderer/src/lib/input-quiet-scheduler.ts index 66ed6c6d90f..c968fe38a8b 100644 --- a/src/renderer/src/lib/input-quiet-scheduler.ts +++ b/src/renderer/src/lib/input-quiet-scheduler.ts @@ -15,6 +15,7 @@ const INPUT_QUIET_EVENTS: readonly (keyof WindowEventMap)[] = [ ] let listenersInstalled = false +let listenersWindow: Window | null = null let lastInputAt = Number.NEGATIVE_INFINITY function now(): number { @@ -49,6 +50,25 @@ function ensureInputQuietListeners(targetWindow: Window): void { for (const eventName of INPUT_QUIET_EVENTS) { targetWindow.addEventListener(eventName, recordInput, options) } + listenersWindow = targetWindow +} + +function disposeInputQuietListeners(): void { + if (!listenersWindow) { + return + } + const options: AddEventListenerOptions = { capture: true } + for (const eventName of INPUT_QUIET_EVENTS) { + listenersWindow.removeEventListener(eventName, recordInput, options) + } + listenersWindow = null + listenersInstalled = false +} + +if (import.meta !== undefined && import.meta.hot) { + // Vite can replace this module without a full renderer reload. Remove the + // global input hooks so dev sessions do not retain stale scheduler closures. + import.meta.hot.dispose(disposeInputQuietListeners) } function scheduleIdleCallback(targetWindow: Window, callback: () => void, timeout: number): number { diff --git a/src/renderer/src/lib/keyboard-layout/layout-base-character.ts b/src/renderer/src/lib/keyboard-layout/layout-base-character.ts index 8f05f9ba827..3fdc8206c9a 100644 --- a/src/renderer/src/lib/keyboard-layout/layout-base-character.ts +++ b/src/renderer/src/lib/keyboard-layout/layout-base-character.ts @@ -108,6 +108,22 @@ function refreshOnFocus(): void { void refreshLayoutMap() } +function disposeLayoutCharacterPrefetch(): void { + if (attachedWindow) { + attachedWindow.removeEventListener('focus', refreshOnFocus) + attachedWindow = null + } + unsubscribeLayoutChange?.() + unsubscribeLayoutChange = null + focusListenerAttached = false +} + +if (import.meta !== undefined && import.meta.hot) { + // Vite can replace this module without a full renderer reload. Remove the + // global focus/layout hooks so dev sessions do not accumulate listeners. + import.meta.hot.dispose(disposeLayoutCharacterPrefetch) +} + /** A layout map entry is usable as a kitty base key only if it is a single * printable codepoint (dead keys report names like 'Dead'; some entries are * empty). Exposed for tests. */ 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/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/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/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-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) 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/src/renderer/src/store/slices/terminal-pane-retirement-ownership.test.ts b/src/renderer/src/store/slices/terminal-pane-retirement-ownership.test.ts new file mode 100644 index 00000000000..ee21460d8c7 --- /dev/null +++ b/src/renderer/src/store/slices/terminal-pane-retirement-ownership.test.ts @@ -0,0 +1,85 @@ +import { expect, it } from 'vitest' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import { + getTerminalPtyOwnershipIdentity, + hasTerminalPtyOwnerOutsidePane, + type TerminalTabRetirementState +} from './terminal-tab-retirement' + +function tab(id: string, ptyId: string | null = null): TerminalTab { + return { + id, + worktreeId: 'wt', + ptyId, + title: id, + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 0 + } +} +function state(): TerminalTabRetirementState { + return { + worktreesByRepo: { repo: [{ id: 'wt', repoId: 'repo', runtimeOwnerEnvironmentId: 'env-1' }] }, + tabsByWorktree: { wt: [tab('closing', 'remote:env-1@@session')] }, + unifiedTabsByWorktree: {}, + ptyIdsByTabId: { closing: ['remote:env-1@@session'] }, + terminalLayoutsByTabId: { + closing: { + root: null, + activeLeafId: null, + expandedLeafId: null, + ptyIdsByLeafId: { original: 'remote:env-1@@session' } + } + }, + lastKnownRelayPtyIdByTabId: {}, + deferredSshSessionIdsByTabId: {}, + pendingReconnectPtyIdByTabId: {} + } +} + +it('ignores the closing leaf and its aggregate row, but protects a sibling legacy alias', () => { + const current = state() + const identity = getTerminalPtyOwnershipIdentity(current, 'remote:env-1@@session', 'wt') + expect(hasTerminalPtyOwnerOutsidePane(current, identity, 'closing', 'original')).toBe(false) + current.terminalLayoutsByTabId.closing.ptyIdsByLeafId = { + original: 'remote:env-1@@session', + sibling: 'remote:session' + } + expect(hasTerminalPtyOwnerOutsidePane(current, identity, 'closing', 'original')).toBe(true) +}) + +it('the late check protects replacement ownership at the same durable leaf', () => { + const current = state() + const identity = getTerminalPtyOwnershipIdentity(current, 'remote:session', 'wt') + expect(hasTerminalPtyOwnerOutsidePane(current, identity, 'closing')).toBe(true) +}) + +it.each(['row', 'layout', 'aggregate', 'relay', 'deferred', 'reconnect'] as const)( + 'protects another live tab with only %s ownership', + (source) => { + const current = state() + const id = 'remote:session' + current.tabsByWorktree.wt.push(tab('survivor', source === 'row' ? id : null)) + if (source === 'layout') { + current.terminalLayoutsByTabId.survivor = { + root: null, + activeLeafId: null, + expandedLeafId: null, + ptyIdsByLeafId: { leaf: id } + } + } else if (source === 'aggregate') { + current.ptyIdsByTabId.survivor = [id] + } else if (source === 'relay') { + current.lastKnownRelayPtyIdByTabId.survivor = id + } else if (source === 'deferred') { + current.deferredSshSessionIdsByTabId.survivor = id + } else if (source === 'reconnect') { + current.pendingReconnectPtyIdByTabId.survivor = id + } + const identity = getTerminalPtyOwnershipIdentity(current, 'remote:env-1@@session', 'wt') + expect(hasTerminalPtyOwnerOutsidePane(current, identity, 'closing', 'original')).toBe(true) + current.tabsByWorktree.wt.pop() + expect(hasTerminalPtyOwnerOutsidePane(current, identity, 'closing', 'original')).toBe(false) + } +) diff --git a/src/renderer/src/store/slices/terminal-tab-retirement.ts b/src/renderer/src/store/slices/terminal-tab-retirement.ts index 80e67824699..e2e034fa6af 100644 --- a/src/renderer/src/store/slices/terminal-tab-retirement.ts +++ b/src/renderer/src/store/slices/terminal-tab-retirement.ts @@ -135,6 +135,30 @@ export function isTerminalTabPresent( return locateTerminalTab(state.tabsByWorktree, tabId) !== null } +export function hasTerminalPtyOwnerOutsidePane( + state: TerminalTabRetirementState, + identity: string, + tabId: string, + excludedLeafId?: string +): boolean { + for (const [ownerTabId, owner] of collectLiveTerminalTabs(state)) { + const ids = + ownerTabId === tabId + ? Object.entries(state.terminalLayoutsByTabId[tabId]?.ptyIdsByLeafId ?? {}) + .filter(([leafId]) => leafId !== excludedLeafId) + .map(([, ptyId]) => ptyId) + : collectPtyIdsForTab(state, ownerTabId, owner.rowPtyId) + if ( + ids.some( + (ptyId) => getTerminalPtyOwnershipIdentity(state, ptyId, owner.worktreeId) === identity + ) + ) { + return true + } + } + return false +} + export function buildTerminalTabRetirementPlan( state: TerminalTabRetirementState, tabId: string diff --git a/src/renderer/src/store/slices/ui/ui-slice-contract-core.ts b/src/renderer/src/store/slices/ui/ui-slice-contract-core.ts index 2506e5dd071..ffb37c40a01 100644 --- a/src/renderer/src/store/slices/ui/ui-slice-contract-core.ts +++ b/src/renderer/src/store/slices/ui/ui-slice-contract-core.ts @@ -99,6 +99,9 @@ export type NewWorkspaceDraft = { linkedGitLabMR?: number | null // Why: repo-scoped start ref from the "Start from" picker; absent means "use the repo's effective base ref". baseBranch?: string + // Why: false when `baseBranch` came from the base-ref picker rather than a branch pick, so restoring the + // draft doesn't turn it back into a name-field pill. Absent on pre-flag drafts, which restore as a pick. + baseBranchNamesWorkspace?: boolean // Why: review worktrees start from a head ref/SHA while Source Control compares against the provider target branch. compareBaseRef?: string } 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/renderer/src/components/sidebar/worktree-record-selector-cache.ts b/src/renderer/src/store/worktree-record-selector-cache.ts similarity index 85% rename from src/renderer/src/components/sidebar/worktree-record-selector-cache.ts rename to src/renderer/src/store/worktree-record-selector-cache.ts index 220d2d074bf..401eb772f91 100644 --- a/src/renderer/src/components/sidebar/worktree-record-selector-cache.ts +++ b/src/renderer/src/store/worktree-record-selector-cache.ts @@ -6,6 +6,14 @@ type WorktreeRecordGeneration = { byWorktreeId: Map } +/** Why not `Object.keys`: a `Set`/`Map` value has none, so the default check + * would collapse every non-empty one onto the shared empty identity. */ +function isEmptyValue(value: TValue): boolean { + return value instanceof Set || value instanceof Map + ? value.size === 0 + : Object.keys(value).length === 0 +} + function sameSources(previous: readonly unknown[], next: readonly unknown[]): boolean { if (previous.length !== next.length) { return false @@ -52,7 +60,7 @@ export function createWorktreeRecordSelector(opti const built = options.build(state, worktreeId) const carried = generation.carried?.get(worktreeId) let value = built - if (Object.keys(built).length === 0) { + if (isEmptyValue(built)) { value = options.empty } else if (carried && shallow(carried, built)) { value = carried diff --git a/src/renderer/src/web/preload-api/web-agent-accounts-api.ts b/src/renderer/src/web/preload-api/web-agent-accounts-api.ts index 26b11ad68dc..8738039de61 100644 --- a/src/renderer/src/web/preload-api/web-agent-accounts-api.ts +++ b/src/renderer/src/web/preload-api/web-agent-accounts-api.ts @@ -27,18 +27,42 @@ export function createGrokAccountsApi(): NonNullable['grokAc } } -export function createAccountsApi(): never { - const empty = { +function createEmptyManagedAccountsState(): { + accounts: never[] + activeAccountId: null + activeAccountIdsByRuntime: { host: null; wsl: Record } +} { + return { accounts: [], activeAccountId: null, activeAccountIdsByRuntime: { host: null, wsl: {} } } +} + +export function createClaudeAccountsApi(): PreloadApi['claudeAccounts'] { + const empty = createEmptyManagedAccountsState() return { list: () => Promise.resolve(empty), add: () => Promise.resolve(empty), cancelPendingLogin: () => Promise.resolve(false), reauthenticate: () => Promise.resolve(empty), remove: () => Promise.resolve(empty), + select: () => Promise.resolve(empty) + } +} + +export function createCodexAccountsApi(): PreloadApi['codexAccounts'] { + const empty = createEmptyManagedAccountsState() + return { + list: () => Promise.resolve(empty), + add: () => Promise.resolve(empty), + cancelPendingLogin: () => Promise.resolve(false), + // Why: the login runs on the desktop host that owns the browser, so a web + // client has no link to offer and nothing to publish changes from. + getPendingLoginUrl: () => Promise.resolve(null), + onPendingLoginUrlChanged: () => () => {}, + reauthenticate: () => Promise.resolve(empty), + remove: () => Promise.resolve(empty), select: () => Promise.resolve(empty), // Why: launch accounts are recorded on the host that owns the PTY, which the // web client never is — report no stale panes rather than reject the sweep. @@ -47,5 +71,5 @@ export function createAccountsApi(): never { // client has no recorded lane to offer and every pane falls to derivation. listRecordedPaneLanes: () => Promise.resolve({}), forgetStalePanes: () => Promise.resolve() - } as never + } } diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index a10ecb0d478..0f6848a1827 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -2,7 +2,8 @@ import type { PreloadApi } from '../../../preload/api-types' import type { StatsSummary } from '../../../shared/process-stats-types' import { createWebE2EApi } from './preload-api/web-e2e-api' import { - createAccountsApi, + createClaudeAccountsApi, + createCodexAccountsApi, createGrokAccountsApi, createMiniMaxCredentialsApi } from './preload-api/web-agent-accounts-api' @@ -106,8 +107,8 @@ function createWebPreloadApi(): Partial { rateLimits: createRateLimitsApi(), minimaxCredentials: createMiniMaxCredentialsApi(), grokAccounts: createGrokAccountsApi(), - codexAccounts: createAccountsApi(), - claudeAccounts: createAccountsApi(), + codexAccounts: createCodexAccountsApi(), + claudeAccounts: createClaudeAccountsApi(), cli: createCliApi(), macosTccPrompts: createMacosTccPromptsApi(), codexConfigSync: { diff --git a/src/shared/agent-session-journal-schemas.test.ts b/src/shared/agent-session-journal-schemas.test.ts index a575e4525fe..4a41437a3b4 100644 --- a/src/shared/agent-session-journal-schemas.test.ts +++ b/src/shared/agent-session-journal-schemas.test.ts @@ -84,6 +84,19 @@ const CANONICAL_BODIES: AgentJournalItemBody[] = [ kind: 'status', text: 'turn', turnLifecycle: { turnId: 'turn-3', state: 'unverifiable', startedAt: 1_000 } + }, + { + kind: 'status', + text: 'turn', + turnLifecycle: { turnId: 'turn-4', state: 'completed', outcome: 'failure', startedAt: 1_000 } + }, + { + kind: 'turn', + turnId: 'turn-5', + state: 'interrupted', + outcome: 'cancellation', + startedAt: 1_000, + completedAt: 2_000 } ] @@ -248,6 +261,30 @@ describe('forward tolerance', () => { }) ).toBe(true) }) + + it('admits a turn outcome from a later vocabulary but rejects a non-string one', () => { + // Open like `state`: a verdict a newer build writes keeps the row readable, + // and `readAgentJournalTurnOutcome` is what stops it being acted on. A + // non-string stays fatal — the row is structurally wrong, not just newer. + const turn = { kind: 'turn', turnId: 'turn-1', state: 'completed' } + expect(isAdmissibleAgentJournalItemBody({ ...turn, outcome: 'partially-refused' })).toBe(true) + expect(isAdmissibleAgentJournalItemBody({ ...turn, outcome: 7 })).toBe(false) + expect(isAdmissibleAgentJournalItemBody({ ...turn, outcome: '' })).toBe(false) + expect( + isAdmissibleAgentJournalItemBody({ + kind: 'status', + text: 'turn', + turnLifecycle: { turnId: 'turn-1', state: 'completed', outcome: 'partially-refused' } + }) + ).toBe(true) + expect( + isAdmissibleAgentJournalItemBody({ + kind: 'status', + text: 'turn', + turnLifecycle: { turnId: 'turn-1', state: 'completed', outcome: 7 } + }) + ).toBe(false) + }) }) describe('optional notice metadata', () => { diff --git a/src/shared/agent-session-journal-schemas.ts b/src/shared/agent-session-journal-schemas.ts index f355e6c1bb0..ca95cd2892c 100644 --- a/src/shared/agent-session-journal-schemas.ts +++ b/src/shared/agent-session-journal-schemas.ts @@ -211,6 +211,7 @@ export const AgentJournalItemBodySchema = z.discriminatedUnion('kind', [ .object({ turnId: z.string(), state: z.string().min(1), + outcome: z.string().min(1).optional(), userItemId: z.string().min(1).optional(), startedAt: z.number().finite().positive().optional(), requestedAt: z.number().finite().positive().optional(), @@ -224,6 +225,10 @@ export const AgentJournalItemBodySchema = z.discriminatedUnion('kind', [ kind: z.literal('turn'), turnId: z.string(), state: z.string().min(1), + // Open like `state`: a verdict a newer build writes must not turn the row + // malformed. `readAgentJournalTurnOutcome` is where an unplaceable one + // becomes unknown rather than an arm a caller would act on. + outcome: z.string().min(1).optional(), userItemId: z.string().min(1).optional(), startedAt: z.number().finite().positive().optional(), requestedAt: z.number().finite().positive().optional(), diff --git a/src/shared/agent-session-journal-types.ts b/src/shared/agent-session-journal-types.ts index a491d0d7286..287e07e2a5d 100644 --- a/src/shared/agent-session-journal-types.ts +++ b/src/shared/agent-session-journal-types.ts @@ -184,9 +184,22 @@ export const AGENT_JOURNAL_TURN_LIFECYCLE_STATES = [ ] as const export type AgentJournalTurnLifecycleState = (typeof AGENT_JOURNAL_TURN_LIFECYCLE_STATES)[number] +/** What the PROVIDER said became of a turn, kept separate from the lifecycle + * state so the four arms above stay a report on what the HOST observed. + * `cancellation` is a stop somebody asked for, `failure` is the provider's own + * error, and the two are never interchangeable: only `failure` is a fault. */ +export const AGENT_JOURNAL_TURN_OUTCOMES = ['success', 'failure', 'cancellation'] as const +export type AgentJournalTurnOutcome = (typeof AGENT_JOURNAL_TURN_OUTCOMES)[number] + export type AgentJournalTurnLifecycle = { turnId: string state: AgentJournalTurnLifecycleState + /** The provider's own verdict, when it gave one. ABSENT MEANS UNKNOWN and must + * never be read as success: a row from a host that predates the field, an end + * the host inferred rather than heard, and a verdict vocabulary this build + * cannot place all land here. `completed` alone proves nothing — the provider + * reports an API error as a finished turn. */ + outcome?: AgentJournalTurnOutcome /** Journal key of the user item that opened the turn. A lifecycle row may key * itself when provider output opened a turn with no user item; absent means * an older host. */ @@ -224,7 +237,8 @@ export type AgentJournalStatusItem = { * never tombstoned, so the endpoints survive. Timestamps are the execution * host's clock at provider-event receipt; `durationMs` is the provider's own * measurement. `unverifiable` carries no end: the host lost the child without - * observing its exit. */ + * observing its exit. `outcome` is the provider's separate verdict and is + * absent whenever nothing told the host one. */ export type AgentJournalTurnItem = { kind: 'turn' } & AgentJournalTurnLifecycle export type AgentJournalItemBody = diff --git a/src/shared/agent-session-turn-record.test.ts b/src/shared/agent-session-turn-record.test.ts index 48cf99097a2..bcd14a7290f 100644 --- a/src/shared/agent-session-turn-record.test.ts +++ b/src/shared/agent-session-turn-record.test.ts @@ -8,7 +8,8 @@ import { agentJournalTurnBody, isRunningAgentJournalTurn, legacyAgentJournalTurnStatusBody, - readAgentJournalTurn + readAgentJournalTurn, + readAgentJournalTurnOutcome } from './agent-session-turn-record' const turn: AgentJournalTurnLifecycle = { @@ -48,6 +49,52 @@ describe('readAgentJournalTurn', () => { }) }) +describe('readAgentJournalTurnOutcome', () => { + it.each(['success', 'failure', 'cancellation'] as const)( + 'reads a %s verdict off either journal shape', + (outcome) => { + const withOutcome = { ...turn, outcome } + expect( + readAgentJournalTurnOutcome(readAgentJournalTurn(agentJournalTurnBody(withOutcome))) + ).toBe(outcome) + expect( + readAgentJournalTurnOutcome( + readAgentJournalTurn({ kind: 'status', text: 'x', turnLifecycle: withOutcome }) + ) + ).toBe(outcome) + } + ) + + it('never reads an old host completed row as success', () => { + // The mixed-version case this field exists for. Hosts that predate it wrote + // `completed` for a turn the provider ended with an API error, so a newer + // client must read those as UNKNOWN. Collapsing absent into success would + // make every historical failure indistinguishable from a clean turn. + expect(readAgentJournalTurnOutcome(turn)).toBeNull() + expect(readAgentJournalTurnOutcome({ turnId: 't', state: 'completed' })).toBeNull() + expect( + readAgentJournalTurnOutcome( + readAgentJournalTurn({ kind: 'status', text: 'Claude turn completed', turnLifecycle: turn }) + ) + ).toBeNull() + }) + + it('reads a verdict from a later vocabulary as unknown rather than an arm', () => { + // The persisted field is an open string so a newer host's row stays readable. + // That means the decoded value is typed as an arm this build knows without + // having been checked against one, and this reader is the check. + expect(readAgentJournalTurnOutcome({ ...turn, outcome: 'partially-refused' })).toBeNull() + }) + + it('answers unknown for a body that carries no turn at all', () => { + expect(readAgentJournalTurnOutcome(null)).toBeNull() + expect(readAgentJournalTurnOutcome(undefined)).toBeNull() + expect( + readAgentJournalTurnOutcome(readAgentJournalTurn({ kind: 'status', text: 'compacted' })) + ).toBeNull() + }) +}) + describe('legacyAgentJournalTurnStatusBody', () => { it('names the agent from the lifecycle identity and never calls an unobserved end completed', () => { expect(legacyAgentJournalTurnStatusBody(turn, 'legacy:claude:s:turn-lifecycle%3At1')).toEqual({ @@ -62,6 +109,21 @@ describe('legacyAgentJournalTurnStatusBody', () => { ).text ).toBe('Codex turn outcome unverifiable') }) + + it('carries the verdict to a client that predates the turn item', () => { + // The downgrade is the only carrier an old client gets. Its own text still + // reads "completed" — that copy is the pre-existing contract and this change + // does not move it — but the verdict travels under `turnLifecycle`, so a + // client that learns to read it needs no host change. + const failed = { ...turn, outcome: 'failure' as const } + expect(legacyAgentJournalTurnStatusBody(failed, 'legacy:claude:s:turn-lifecycle%3At1')).toEqual( + { + kind: 'status', + text: 'Claude turn completed', + turnLifecycle: failed + } + ) + }) }) describe('journalRowSchemaVersion', () => { diff --git a/src/shared/agent-session-turn-record.ts b/src/shared/agent-session-turn-record.ts index 177acbb5e52..d321b663b7b 100644 --- a/src/shared/agent-session-turn-record.ts +++ b/src/shared/agent-session-turn-record.ts @@ -2,11 +2,13 @@ // item this build writes, and the status row with `turnLifecycle` that older // hosts wrote and that older clients still receive. -import type { - AgentJournalItemBody, - AgentJournalStatusItem, - AgentJournalTurnItem, - AgentJournalTurnLifecycle +import { + AGENT_JOURNAL_TURN_OUTCOMES, + type AgentJournalItemBody, + type AgentJournalStatusItem, + type AgentJournalTurnItem, + type AgentJournalTurnLifecycle, + type AgentJournalTurnOutcome } from './agent-session-journal-types' import { agentTurnLifecycleText } from './agent-turn-lifecycle-text' @@ -27,6 +29,32 @@ export function isRunningAgentJournalTurn(body: AgentJournalItemBody | undefined return readAgentJournalTurn(body)?.state === 'running' } +/** A turn record as DECODED, with the verdict left open. The journal schema + * type-checks `outcome` as a string and never against the arm set, so this is + * the shape a reader really holds — `AgentJournalTurnLifecycle` is assignable + * to it, and narrowing is what the reader below is for. */ +export type AgentJournalTurnOutcomeSource = Omit & { + outcome?: string +} + +/** + * The provider's verdict on a turn, or null when nothing recorded one. + * + * The only safe way to ask. The persisted field is an open string so a newer + * host's row stays readable, which means the decoded value is typed as an arm + * this build knows without having been checked against one. Everything that + * cannot be placed — absent, an arm from a later vocabulary, a legacy row whose + * `completed` was written before outcomes existed — answers null, and null is + * UNKNOWN. A caller must never treat it as success: the host writes `completed` + * for a turn the provider ended with an API error. + */ +export function readAgentJournalTurnOutcome( + turn: AgentJournalTurnOutcomeSource | null | undefined +): AgentJournalTurnOutcome | null { + const outcome = turn?.outcome + return AGENT_JOURNAL_TURN_OUTCOMES.find((known) => known === outcome) ?? null +} + export function agentJournalTurnBody(turn: AgentJournalTurnLifecycle): AgentJournalTurnItem { return { kind: 'turn', ...turn } } 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/codex-auth-errors.ts b/src/shared/codex-auth-errors.ts index 366a129c3cb..ba18b5704fe 100644 --- a/src/shared/codex-auth-errors.ts +++ b/src/shared/codex-auth-errors.ts @@ -1,3 +1,9 @@ +/** Carries the sign-in link of an in-flight `codex login` from main to every window. */ +export const CODEX_PENDING_LOGIN_URL_CHANGED_CHANNEL = 'codexAccounts:pendingLoginUrlChanged' + +/** The rejection a cancelled `codex login` produces; the Accounts pane reads it to keep a cancellation out of the error toast. */ +export const CODEX_LOGIN_CANCELLED_MESSAGE = 'Codex sign-in was cancelled.' + const CODEX_AUTH_ERROR_PATTERNS = [ /access token could not be refreshed/i, /authentication session could not be refreshed/i, 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/execution-host.ts b/src/shared/execution-host.ts index bbe55aea1a0..7c9f3857d07 100644 --- a/src/shared/execution-host.ts +++ b/src/shared/execution-host.ts @@ -155,9 +155,12 @@ export function normalizeExecutionHostOrder( return normalized ?? [] } -export function getRepoExecutionHostId( - repo: Pick -): ExecutionHostId { +// Why the loose member types: a reply reader hands these through as the strings they are on the +// wire, and this function is already the thing that decides what an unparseable spelling means. +export function getRepoExecutionHostId(repo: { + connectionId?: string | null + executionHostId?: string | null +}): ExecutionHostId { const executionHostId = normalizeExecutionHostId(repo.executionHostId) if (executionHostId) { return executionHostId 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( diff --git a/src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts b/src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts new file mode 100644 index 00000000000..a4c1abddbb2 --- /dev/null +++ b/src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from 'vitest' +import { sha256 } from '../sha256' +import { + computeMobileWebBundleId, + MOBILE_WEB_BUNDLE_ENTRYPOINT, + MOBILE_WEB_BUNDLE_SCHEMA_VERSION, + type MobileWebBundleAsset +} from './manifest-contract' +import { + MobileWebBundleChunkParamsSchema, + MobileWebBundleChunkResultSchema, + MobileWebBundleErrorCodeSchema, + MobileWebBundleManifestResultSchema, + MOBILE_WEB_BUNDLE_CHUNK_BYTES, + MOBILE_WEB_BUNDLE_CHUNK_METHOD, + MOBILE_WEB_BUNDLE_ERROR_CODES, + MOBILE_WEB_BUNDLE_MANIFEST_METHOD +} from './bundle-rpc-contract' +import { MOBILE_WEB_BUNDLE_CAPABILITY } from './mobile-web-bundle-capability' + +const BUILD_ID = 'a'.repeat(64) +const MAX_DATA_BASE64_LENGTH = Math.ceil(MOBILE_WEB_BUNDLE_CHUNK_BYTES / 3) * 4 + 8 + +function hexDigest(input: string): string { + return Array.from(sha256(new TextEncoder().encode(input)), (byte) => + byte.toString(16).padStart(2, '0') + ).join('') +} + +const ENTRY_ASSET: MobileWebBundleAsset = { + path: MOBILE_WEB_BUNDLE_ENTRYPOINT, + sha256: hexDigest(MOBILE_WEB_BUNDLE_ENTRYPOINT), + byteLength: 64, + contentType: 'text/html; charset=utf-8' +} + +const VALID_MANIFEST = { + schemaVersion: MOBILE_WEB_BUNDLE_SCHEMA_VERSION, + buildId: computeMobileWebBundleId([ENTRY_ASSET]), + desktopVersion: '1.4.200', + minCompatibleRuntimeProtocolVersion: 3, + runtimeProtocolVersion: 3, + entrypoint: MOBILE_WEB_BUNDLE_ENTRYPOINT, + totalBytes: ENTRY_ASSET.byteLength, + assets: [ENTRY_ASSET] +} + +function chunkResult(overrides: Record = {}): Record { + return { + buildId: BUILD_ID, + path: 'assets/a.js', + offset: 0, + assetByteLength: 1024, + sha256: 'b'.repeat(64), + dataBase64: 'AAAA', + eof: true, + ...overrides + } +} + +describe('names and sizes', () => { + it('pins the wire constants', () => { + expect(MOBILE_WEB_BUNDLE_CHUNK_BYTES).toBe(49152) + expect(MOBILE_WEB_BUNDLE_MANIFEST_METHOD).toBe('mobileWeb.bundle.manifest') + expect(MOBILE_WEB_BUNDLE_CHUNK_METHOD).toBe('mobileWeb.bundle.chunk') + expect(MOBILE_WEB_BUNDLE_CAPABILITY).toBe('mobileWeb.bundle.v1') + }) +}) + +describe('MobileWebBundleErrorCodeSchema', () => { + it('round-trips every code', () => { + expect(MOBILE_WEB_BUNDLE_ERROR_CODES).toHaveLength(6) + for (const code of MOBILE_WEB_BUNDLE_ERROR_CODES) { + expect(MobileWebBundleErrorCodeSchema.parse(code)).toBe(code) + } + }) + + it('is closed', () => { + expect(MobileWebBundleErrorCodeSchema.safeParse('mobile_web_bundle_unknown').success).toBe( + false + ) + expect(MobileWebBundleErrorCodeSchema.safeParse('').success).toBe(false) + }) +}) + +describe('mobileWeb.bundle.manifest payloads', () => { + 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) + expect(parsed.success).toBe(true) + expect(parsed.success && parsed.data.manifest.buildId).toBe(VALID_MANIFEST.buildId) + expect(MobileWebBundleManifestResultSchema.safeParse({ ...reply, manifest: {} }).success).toBe( + false + ) + expect(MobileWebBundleManifestResultSchema.safeParse({ ...reply, extra: 1 }).success).toBe( + false + ) + }) + + it('allows a shrunk chunk size but not one past the constant', () => { + const reply = { manifest: VALID_MANIFEST, chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES } + expect( + MobileWebBundleManifestResultSchema.safeParse({ ...reply, chunkBytes: 8 * 1024 }).success + ).toBe(true) + expect( + MobileWebBundleManifestResultSchema.safeParse({ + ...reply, + chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES + 1 + }).success + ).toBe(false) + expect(MobileWebBundleManifestResultSchema.safeParse({ ...reply, chunkBytes: 0 }).success).toBe( + false + ) + }) +}) + +describe('mobileWeb.bundle.chunk params', () => { + const params = { buildId: BUILD_ID, path: 'assets/a.js', offset: 0 } + + it('accepts a well-formed request', () => { + expect(MobileWebBundleChunkParamsSchema.safeParse(params).success).toBe(true) + }) + + it('is strict and bounded', () => { + expect(MobileWebBundleChunkParamsSchema.safeParse({ ...params, gzip: true }).success).toBe( + false + ) + expect(MobileWebBundleChunkParamsSchema.safeParse({ ...params, offset: -1 }).success).toBe( + false + ) + expect(MobileWebBundleChunkParamsSchema.safeParse({ ...params, offset: 1.5 }).success).toBe( + false + ) + expect( + MobileWebBundleChunkParamsSchema.safeParse({ ...params, path: '../escape.js' }).success + ).toBe(false) + expect(MobileWebBundleChunkParamsSchema.safeParse({ ...params, buildId: 'abc' }).success).toBe( + false + ) + }) + + it('does not pin offset to the constant chunk size, so the host may shrink it', () => { + expect(MobileWebBundleChunkParamsSchema.safeParse({ ...params, offset: 1024 }).success).toBe( + true + ) + }) +}) + +describe('mobileWeb.bundle.chunk result', () => { + it('accepts a self-describing chunk', () => { + expect(MobileWebBundleChunkResultSchema.safeParse(chunkResult()).success).toBe(true) + }) + + it('accepts base64 of a full chunk and rejects one character past the bound', () => { + const full = Buffer.alloc(MOBILE_WEB_BUNDLE_CHUNK_BYTES).toString('base64') + expect(full.length).toBeLessThanOrEqual(MAX_DATA_BASE64_LENGTH) + expect( + MobileWebBundleChunkResultSchema.safeParse(chunkResult({ dataBase64: full })).success + ).toBe(true) + + const atBound = 'A'.repeat(MAX_DATA_BASE64_LENGTH) + expect( + MobileWebBundleChunkResultSchema.safeParse(chunkResult({ dataBase64: atBound })).success + ).toBe(true) + expect( + MobileWebBundleChunkResultSchema.safeParse(chunkResult({ dataBase64: `${atBound}A` })).success + ).toBe(false) + }) + + it('leaves the padding slack the +8 term buys, so the host enforces the chunk size', () => { + const overshoot = Buffer.alloc(MOBILE_WEB_BUNDLE_CHUNK_BYTES + 1).toString('base64') + expect(overshoot.length).toBeLessThanOrEqual(MAX_DATA_BASE64_LENGTH) + const wellPast = Buffer.alloc(MOBILE_WEB_BUNDLE_CHUNK_BYTES + 64).toString('base64') + expect( + MobileWebBundleChunkResultSchema.safeParse(chunkResult({ dataBase64: wellPast })).success + ).toBe(false) + }) + + it('is strict and requires every echoed field', () => { + expect( + MobileWebBundleChunkResultSchema.safeParse(chunkResult({ contentEncoding: 'gzip' })).success + ).toBe(false) + for (const key of [ + 'buildId', + 'path', + 'offset', + 'assetByteLength', + 'sha256', + 'dataBase64', + 'eof' + ]) { + const partial = chunkResult() + delete partial[key] + expect(MobileWebBundleChunkResultSchema.safeParse(partial).success).toBe(false) + } + }) +}) diff --git a/src/shared/mobile-web-bundle/bundle-rpc-contract.ts b/src/shared/mobile-web-bundle/bundle-rpc-contract.ts new file mode 100644 index 00000000000..4395a4e9d1c --- /dev/null +++ b/src/shared/mobile-web-bundle/bundle-rpc-contract.ts @@ -0,0 +1,78 @@ +import { z } from 'zod' +import { hostUnionArms } from '../zod-salvage' +import { + MobileWebBundleAssetPathSchema, + MobileWebBundleManifestSchema, + MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES +} from './manifest-contract' + +/** 48 KiB survives the compounded ~1.78x expansion (base64 body inside a base64 mobile E2EE reply) + * 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' + +const SHA256_PATTERN = /^[a-f0-9]{64}$/ +const MAX_DATA_BASE64_LENGTH = Math.ceil(MOBILE_WEB_BUNDLE_CHUNK_BYTES / 3) * 4 + 8 + +export type MobileWebBundleErrorCode = + | 'mobile_web_bundle_unavailable' + | 'mobile_web_bundle_build_changed' + | 'mobile_web_bundle_asset_unknown' + | 'mobile_web_bundle_asset_changed' + | 'mobile_web_bundle_offset_invalid' + | 'mobile_web_bundle_read_limited' + +/** Coverage record, so tsc fails on an arm added to the union without a schema arm and vice versa. */ +export const MOBILE_WEB_BUNDLE_ERROR_CODES = hostUnionArms({ + mobile_web_bundle_unavailable: true, + mobile_web_bundle_build_changed: true, + mobile_web_bundle_asset_unknown: true, + mobile_web_bundle_asset_changed: true, + mobile_web_bundle_offset_invalid: true, + mobile_web_bundle_read_limited: true +}) + +export const MobileWebBundleErrorCodeSchema = z.enum(MOBILE_WEB_BUNDLE_ERROR_CODES) + +export const MobileWebBundleManifestResultSchema = z + .object({ + manifest: MobileWebBundleManifestSchema, + /** Read, never assumed, so the host can shrink it without a client release. Capped at the + * constant because a larger value would overshoot the chunk reply's `dataBase64` bound. */ + chunkBytes: z.number().int().positive().max(MOBILE_WEB_BUNDLE_CHUNK_BYTES) + }) + .strict() + +/** No `multipleOf` pin on `offset`: alignment is against the host's advertised `chunkBytes`, which + * may be smaller than the constant, so the host rejects a misaligned offset instead. */ +export const MobileWebBundleChunkParamsSchema = z + .object({ + buildId: z.string().regex(SHA256_PATTERN), + path: MobileWebBundleAssetPathSchema, + offset: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES) + }) + .strict() + +/** Strict, so a later `contentEncoding` is only a Rule 1 optional-field addition for clients whose + * own reply readers are not strict. */ +export const MobileWebBundleChunkResultSchema = z + .object({ + buildId: z.string().regex(SHA256_PATTERN), + path: MobileWebBundleAssetPathSchema, + offset: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), + /** The whole asset, not this chunk: named for it so a reassembler cannot misread the two, and + * paired with `sha256` it describes the asset without a second index. */ + assetByteLength: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), + sha256: z.string().regex(SHA256_PATTERN), + dataBase64: z.string().max(MAX_DATA_BASE64_LENGTH), + eof: z.boolean() + }) + .strict() + +export type MobileWebBundleManifestResult = z.infer +export type MobileWebBundleChunkParams = z.infer +export type MobileWebBundleChunkResult = z.infer diff --git a/src/shared/mobile-web-bundle/manifest-contract.test.ts b/src/shared/mobile-web-bundle/manifest-contract.test.ts new file mode 100644 index 00000000000..350197fa8da --- /dev/null +++ b/src/shared/mobile-web-bundle/manifest-contract.test.ts @@ -0,0 +1,292 @@ +import { describe, expect, it } from 'vitest' +import { sha256 } from '../sha256' +import { + computeMobileWebBundleId, + serializeMobileWebBundleAssets, + MobileWebBundleManifestSchema, + MOBILE_WEB_BUNDLE_ENTRYPOINT, + MOBILE_WEB_BUNDLE_MAX_ASSETS, + MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES, + MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES, + MOBILE_WEB_BUNDLE_SCHEMA_VERSION, + type MobileWebBundleAsset +} from './manifest-contract' + +function hexDigest(input: string): string { + return Array.from(sha256(new TextEncoder().encode(input)), (byte) => + byte.toString(16).padStart(2, '0') + ).join('') +} + +function asset(path: string, byteLength: number): MobileWebBundleAsset { + return { + path, + sha256: hexDigest(path), + byteLength, + contentType: path.endsWith('.html') ? 'text/html; charset=utf-8' : 'text/javascript' + } +} + +const ENTRY = asset(MOBILE_WEB_BUNDLE_ENTRYPOINT, 64) + +function manifestOf( + assets: readonly MobileWebBundleAsset[], + overrides: Record = {} +): Record { + const sorted = [...assets].sort((left, right) => (left.path < right.path ? -1 : 1)) + return { + schemaVersion: MOBILE_WEB_BUNDLE_SCHEMA_VERSION, + buildId: computeMobileWebBundleId(sorted), + desktopVersion: '1.4.200', + minCompatibleRuntimeProtocolVersion: 3, + runtimeProtocolVersion: 3, + entrypoint: MOBILE_WEB_BUNDLE_ENTRYPOINT, + totalBytes: sorted.reduce((sum, entry) => sum + entry.byteLength, 0), + assets: sorted, + ...overrides + } +} + +function assetsTotalling(count: number, totalBytes: number): MobileWebBundleAsset[] { + const others = Array.from({ length: count - 1 }, (_, index) => + asset(`assets/${String(index).padStart(3, '0')}.js`, 0) + ) + return [{ ...ENTRY, byteLength: totalBytes }, ...others] +} + +describe('serializeMobileWebBundleAssets', () => { + const assets = [ENTRY, asset('assets/a.js', 10), asset('assets/b.js', 20)] + + it('is stable under reordered input', () => { + const reversed = assets.toReversed() + const rotated = [assets[1], assets[2], assets[0]] + expect(serializeMobileWebBundleAssets(reversed)).toBe(serializeMobileWebBundleAssets(assets)) + expect(computeMobileWebBundleId(rotated)).toBe(computeMobileWebBundleId(assets)) + expect(computeMobileWebBundleId(reversed)).toMatch(/^[a-f0-9]{64}$/) + }) + + it('serializes in path order with a fixed key order', () => { + expect(serializeMobileWebBundleAssets(assets.toReversed())).toBe( + JSON.stringify([assets[1], assets[2], assets[0]]) + ) + }) + + it('changes the id when any hashed field changes', () => { + const base = computeMobileWebBundleId(assets) + expect(computeMobileWebBundleId([...assets.slice(1), { ...ENTRY, byteLength: 65 }])).not.toBe( + base + ) + expect( + computeMobileWebBundleId([...assets.slice(1), { ...ENTRY, contentType: 'text/plain' }]) + ).not.toBe(base) + expect(computeMobileWebBundleId(assets.slice(1))).not.toBe(base) + }) +}) + +describe('MobileWebBundleManifestSchema', () => { + it('accepts a well-formed manifest', () => { + expect(MobileWebBundleManifestSchema.safeParse(manifestOf([ENTRY])).success).toBe(true) + }) + + it('rejects an unknown key', () => { + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf([ENTRY], { bridge: {} })).success + ).toBe(false) + }) + + it('rejects another schema version', () => { + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf([ENTRY], { schemaVersion: 2 })).success + ).toBe(false) + }) +}) + +describe('contract ceilings', () => { + it('accepts the asset count ceiling and rejects one past it', () => { + const atCeiling = assetsTotalling(MOBILE_WEB_BUNDLE_MAX_ASSETS, 64) + expect(MobileWebBundleManifestSchema.safeParse(manifestOf(atCeiling)).success).toBe(true) + const overCeiling = [...atCeiling, asset('assets/overflow.js', 0)] + expect(overCeiling).toHaveLength(MOBILE_WEB_BUNDLE_MAX_ASSETS + 1) + expect(MobileWebBundleManifestSchema.safeParse(manifestOf(overCeiling)).success).toBe(false) + }) + + it('accepts the per-asset ceiling and rejects one byte past it', () => { + const atCeiling = [{ ...ENTRY, byteLength: MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES }] + expect(MobileWebBundleManifestSchema.safeParse(manifestOf(atCeiling)).success).toBe(true) + const overCeiling = [{ ...ENTRY, byteLength: MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES + 1 }] + expect(MobileWebBundleManifestSchema.safeParse(manifestOf(overCeiling)).success).toBe(false) + }) + + it('accepts the total ceiling and rejects one byte past it', () => { + const perAsset = MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES / 4 + const atCeiling = [ + { ...ENTRY, byteLength: perAsset }, + asset('assets/a.js', perAsset), + asset('assets/b.js', perAsset), + asset('assets/c.js', perAsset) + ] + expect(MobileWebBundleManifestSchema.safeParse(manifestOf(atCeiling)).success).toBe(true) + const overCeiling = [...atCeiling.slice(1), { ...ENTRY, byteLength: perAsset + 1 }] + expect(MobileWebBundleManifestSchema.safeParse(manifestOf(overCeiling)).success).toBe(false) + }) +}) + +describe('manifest invariants', () => { + const twoAssets = [ENTRY, asset('assets/a.js', 10)] + + it('rejects an unsorted or duplicated asset list', () => { + const reversed = [...twoAssets].sort((left, right) => (left.path < right.path ? 1 : -1)) + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf(twoAssets, { assets: reversed })).success + ).toBe(false) + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf(twoAssets, { assets: [ENTRY, ENTRY] })) + .success + ).toBe(false) + }) + + it('rejects a totalBytes that disagrees with the asset sum', () => { + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf(twoAssets, { totalBytes: 0 })).success + ).toBe(false) + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf(twoAssets, { totalBytes: 75 })).success + ).toBe(false) + }) + + it('rejects a manifest whose entrypoint is not listed', () => { + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf([asset('assets/a.js', 10)])).success + ).toBe(false) + }) + + it('rejects paths that collide when case is folded', () => { + const parsed = MobileWebBundleManifestSchema.safeParse( + manifestOf([ENTRY, asset('assets/A.js', 10), asset('assets/a.js', 10)]) + ) + expect(parsed.success).toBe(false) + // Both sort strictly ascending, so it must be the fold check that fires, not the order check. + expect(parsed.error?.issues.map((issue) => issue.message)).toEqual([ + 'asset paths must not collide when case is folded' + ]) + }) + + it('rejects a buildId that is not the content hash of the assets', () => { + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf(twoAssets, { buildId: 'f'.repeat(64) })) + .success + ).toBe(false) + // A stale id: correct for a previous asset list, so only the recomputation catches it. + expect( + MobileWebBundleManifestSchema.safeParse( + manifestOf(twoAssets, { buildId: computeMobileWebBundleId([ENTRY]) }) + ).success + ).toBe(false) + }) + + it('rejects an inverted protocol window', () => { + expect( + MobileWebBundleManifestSchema.safeParse( + manifestOf(twoAssets, { minCompatibleRuntimeProtocolVersion: 4 }) + ).success + ).toBe(false) + }) +}) + +describe('refinement short-circuit', () => { + const WRONG_BUILD_ID = 'f'.repeat(64) + + function issuePaths(manifest: Record): string[] { + const parsed = MobileWebBundleManifestSchema.safeParse(manifest) + expect(parsed.success).toBe(false) + return (parsed.error?.issues ?? []).map((issue) => issue.path.join('.')) + } + + it('reports buildId when every cheaper invariant holds', () => { + const withinCeiling = assetsTotalling(MOBILE_WEB_BUNDLE_MAX_ASSETS, 64) + expect(issuePaths(manifestOf(withinCeiling, { buildId: WRONG_BUILD_ID }))).toEqual(['buildId']) + }) + + it('does not hash an oversized asset list', () => { + const overCeiling = [ + ...assetsTotalling(MOBILE_WEB_BUNDLE_MAX_ASSETS, 64), + asset('assets/overflow.js', 0) + ] + const paths = issuePaths(manifestOf(overCeiling, { buildId: WRONG_BUILD_ID })) + expect(paths).toEqual(['assets']) + expect(paths).not.toContain('buildId') + }) + + it('does not hash once a cheaper invariant has failed', () => { + const twoAssets = [ENTRY, asset('assets/a.js', 10)] + expect(issuePaths(manifestOf(twoAssets, { totalBytes: 0, buildId: WRONG_BUILD_ID }))).toEqual([ + 'totalBytes' + ]) + expect( + issuePaths( + manifestOf(twoAssets, { + minCompatibleRuntimeProtocolVersion: 4, + buildId: WRONG_BUILD_ID + }) + ) + ).toEqual(['minCompatibleRuntimeProtocolVersion']) + }) +}) + +describe('asset paths', () => { + it.each([ + '../escape.js', + 'assets/../../escape.js', + '/absolute.js', + 'assets\\escape.js', + 'a/./b.js', + 'assets/nul.js', + 'assets/CON', + 'assets/lpt1.js', + 'assets/foo.', + 'assets/...' + ])('rejects %s', (path) => { + const parsed = MobileWebBundleManifestSchema.safeParse( + manifestOf([ENTRY, { ...asset('assets/a.js', 10), path }]) + ) + expect(parsed.success).toBe(false) + }) + + it('accepts exactly one spelling of a parameterised content type', () => { + expect( + MobileWebBundleManifestSchema.safeParse( + manifestOf([{ ...ENTRY, contentType: 'text/html; charset=utf-8' }]) + ).success + ).toBe(true) + // A2's builder emits the single-space form; the other spellings are the same bytes under a + // different build id. + for (const contentType of ['text/html;charset=utf-8', 'text/html; charset=utf-8']) { + expect( + MobileWebBundleManifestSchema.safeParse(manifestOf([{ ...ENTRY, contentType }])).success + ).toBe(false) + } + }) + + it('rejects an uppercase content type, which would give the same bytes two ids', () => { + for (const contentType of ['Text/HTML; charset=utf-8', 'text/JavaScript', 'TEXT/PLAIN']) { + expect( + MobileWebBundleManifestSchema.safeParse( + manifestOf([ENTRY, { ...asset('assets/a.js', 10), contentType }]) + ).success + ).toBe(false) + } + }) + + it('rejects a malformed sha256 or content type', () => { + expect( + MobileWebBundleManifestSchema.safeParse( + manifestOf([ENTRY, { ...asset('assets/a.js', 10), sha256: 'AB'.repeat(32) }]) + ).success + ).toBe(false) + expect( + MobileWebBundleManifestSchema.safeParse( + manifestOf([ENTRY, { ...asset('assets/a.js', 10), contentType: 'nope' }]) + ).success + ).toBe(false) + }) +}) diff --git a/src/shared/mobile-web-bundle/manifest-contract.ts b/src/shared/mobile-web-bundle/manifest-contract.ts new file mode 100644 index 00000000000..9b110d28e64 --- /dev/null +++ b/src/shared/mobile-web-bundle/manifest-contract.ts @@ -0,0 +1,185 @@ +import { z } from 'zod' +import { sha256 } from '../sha256' + +/** A reader that sees another value must reject rather than guess at the shape. */ +export const MOBILE_WEB_BUNDLE_SCHEMA_VERSION = 1 as const + +/** The only stable-named asset, and the only one that references the content-addressed names. */ +export const MOBILE_WEB_BUNDLE_ENTRYPOINT = 'index.html' + +// Permanent contract ceilings. They bound host memory at manifest-read time and never move with the +// per-phase build budget, which lives in the build's own verifier. +export const MOBILE_WEB_BUNDLE_MAX_ASSETS = 256 +export const MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES = 32 * 1024 * 1024 +export const MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES = 10 * 1024 * 1024 + +const SHA256_PATTERN = /^[a-f0-9]{64}$/ +const ASSET_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/ +const WINDOWS_RESERVED_SEGMENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i +// One spelling only, lowercase with a single space before `charset`: content type feeds the build +// id, so every accepted variant of the same type is another id for the same bytes. +const CONTENT_TYPE_PATTERN = /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*(?:; charset=[a-z0-9-]+)?$/ +const MAX_ASSET_PATH_LENGTH = 255 +const MAX_CONTENT_TYPE_LENGTH = 128 +const MAX_DESKTOP_VERSION_LENGTH = 64 + +/** Every segment must be a name the bundle root can hold on all three desktop platforms: no + * traversal, and none of the Windows shapes that cannot be created or that resolve to a device. + * The regex already bans absolute paths, backslashes, spaces, and empty segments. */ +function isPortableAssetSegment(segment: string): boolean { + return ( + segment !== '.' && + segment !== '..' && + !segment.endsWith('.') && + !WINDOWS_RESERVED_SEGMENT.test(segment) + ) +} + +export const MobileWebBundleAssetPathSchema = z + .string() + .max(MAX_ASSET_PATH_LENGTH) + .regex(ASSET_PATH_PATTERN) + .refine( + (path) => path.split('/').every(isPortableAssetSegment), + 'asset path segment must be portable across macOS, Linux, and Windows' + ) + +export const MobileWebBundleAssetSchema = z + .object({ + path: MobileWebBundleAssetPathSchema, + sha256: z.string().regex(SHA256_PATTERN), + byteLength: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), + contentType: z.string().min(1).max(MAX_CONTENT_TYPE_LENGTH).regex(CONTENT_TYPE_PATTERN) + }) + .strict() + +export type MobileWebBundleAsset = z.infer + +/** Code-unit order, not `localeCompare`: the sort feeds a content hash, so it must not vary. */ +function compareAssetPaths(left: MobileWebBundleAsset, right: MobileWebBundleAsset): number { + if (left.path === right.path) { + return 0 + } + return left.path < right.path ? -1 : 1 +} + +/** The one input to `buildId`: assets sorted by path, fixed key order, no whitespace. Sorting here + * rather than requiring it of the caller is what makes the id a pure function of content. */ +export function serializeMobileWebBundleAssets(assets: readonly MobileWebBundleAsset[]): string { + return JSON.stringify( + [...assets].sort(compareAssetPaths).map((asset) => ({ + path: asset.path, + sha256: asset.sha256, + byteLength: asset.byteLength, + contentType: asset.contentType + })) + ) +} + +/** Pure-JS sha256 rather than `node:crypto`: Metro ships no Node core shims, so the phone must be + * able to recompute the id from a manifest it cached. */ +export function computeMobileWebBundleId(assets: readonly MobileWebBundleAsset[]): string { + const digest = sha256(new TextEncoder().encode(serializeMobileWebBundleAssets(assets))) + return Array.from(digest, (byte) => byte.toString(16).padStart(2, '0')).join('') +} + +function validateManifestInvariants( + manifest: { + buildId: string + entrypoint: string + totalBytes: number + minCompatibleRuntimeProtocolVersion: number + runtimeProtocolVersion: number + assets: readonly MobileWebBundleAsset[] + }, + context: z.RefinementCtx +): void { + // Cheapest first, and each check returns: the build id below is the only one that hashes, and + // zod runs this refinement even when the array ceiling has already failed. + if (manifest.assets.length > MOBILE_WEB_BUNDLE_MAX_ASSETS) { + return + } + let previousPath: string | null = null + let summedBytes = 0 + const foldedPaths = new Set() + for (const asset of manifest.assets) { + if (previousPath !== null && asset.path <= previousPath) { + context.addIssue({ + code: 'custom', + path: ['assets'], + message: 'assets must be sorted by path and unique' + }) + return + } + // Two paths differing only in case are one file on macOS and Windows, so the host would serve + // the same bytes under two entries and one of the two hashes would never match. + const folded = asset.path.toLocaleLowerCase('en-US') + if (foldedPaths.has(folded)) { + context.addIssue({ + code: 'custom', + path: ['assets'], + message: 'asset paths must not collide when case is folded' + }) + return + } + foldedPaths.add(folded) + previousPath = asset.path + summedBytes += asset.byteLength + } + // Without this the total ceiling bounds nothing: a manifest could declare totalBytes 0 and still + // list 256 assets of 10 MiB each. + if (summedBytes !== manifest.totalBytes) { + context.addIssue({ + code: 'custom', + path: ['totalBytes'], + message: 'totalBytes must equal the sum of asset byte lengths' + }) + return + } + if (!manifest.assets.some((asset) => asset.path === manifest.entrypoint)) { + context.addIssue({ + code: 'custom', + path: ['entrypoint'], + message: 'entrypoint must be one of the listed assets' + }) + return + } + if (manifest.minCompatibleRuntimeProtocolVersion > manifest.runtimeProtocolVersion) { + context.addIssue({ + code: 'custom', + path: ['minCompatibleRuntimeProtocolVersion'], + message: 'protocol window must not be inverted' + }) + return + } + // A stale id survives every other check and would then serve the wrong bytes under a cache key + // the client already trusts. + if (manifest.buildId !== computeMobileWebBundleId(manifest.assets)) { + context.addIssue({ + code: 'custom', + path: ['buildId'], + message: 'buildId must be the content hash of the asset list' + }) + } +} + +/** Closed in both directions: `.strict()` rejects an unknown key and `schemaVersion` is a literal, + * so there is no additive path here. Any manifest change is a `schemaVersion` bump, and a phone + * reading a bundle it cached must treat an unrecognised `schemaVersion` as an unusable bundle to + * re-fetch, never as a crash. */ +export const MobileWebBundleManifestSchema = z + .object({ + schemaVersion: z.literal(MOBILE_WEB_BUNDLE_SCHEMA_VERSION), + buildId: z.string().regex(SHA256_PATTERN), + /** The app version that produced the bundle; the update wall's only honest age source. */ + desktopVersion: z.string().min(1).max(MAX_DESKTOP_VERSION_LENGTH), + minCompatibleRuntimeProtocolVersion: z.number().int().nonnegative(), + runtimeProtocolVersion: z.number().int().nonnegative(), + entrypoint: z.literal(MOBILE_WEB_BUNDLE_ENTRYPOINT), + totalBytes: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES), + assets: z.array(MobileWebBundleAssetSchema).min(1).max(MOBILE_WEB_BUNDLE_MAX_ASSETS) + }) + .strict() + .superRefine(validateManifestInvariants) + +export type MobileWebBundleManifest = z.infer diff --git a/src/shared/mobile-web-bundle/mobile-web-bundle-capability.ts b/src/shared/mobile-web-bundle/mobile-web-bundle-capability.ts new file mode 100644 index 00000000000..bf8c250d224 --- /dev/null +++ b/src/shared/mobile-web-bundle/mobile-web-bundle-capability.ts @@ -0,0 +1,4 @@ +/** Negotiated, never inferred from the desktop version: a build can ship without a bundle. + * Zod-free and dependency-free so `protocol-version.ts` can name it without pulling a schema + * library into the phone's capability path. */ +export const MOBILE_WEB_BUNDLE_CAPABILITY = 'mobileWeb.bundle.v1' 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, 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 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, 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() }) 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 } +} 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 } diff --git a/src/shared/structured-agent-session-outbox.ts b/src/shared/structured-agent-session-outbox.ts index 56fb633dada..b2855ac1021 100644 --- a/src/shared/structured-agent-session-outbox.ts +++ b/src/shared/structured-agent-session-outbox.ts @@ -1,6 +1,9 @@ import type { AgentJournalMessageItem, AgentJournalSubmission } from './agent-session-journal-types' import { agentSessionRefusalOperationState } from './agent-session-refusal-retry' -import type { AgentSessionWireRefusalCode } from './agent-session-wire' +import type { + AgentSessionMutationEnvelope, + AgentSessionWireRefusalCode +} from './agent-session-wire' import { structuredAgentSessionPayloadFingerprint } from './structured-agent-session-mutation' import { DISPATCH_REJECTED_CANCELLED } from './structured-agent-session-dispatch-rejection' @@ -193,10 +196,17 @@ export function parseStructuredAgentSessionOutboxEntry( } } -export function structuredAgentSessionSendRequest( +export type StructuredAgentSessionSendMutation = { + envelope: AgentSessionMutationEnvelope + body: AgentJournalMessageItem +} + +/** The `agentSession.send` arguments an entry stands for. Typed rather than wire-shaped so a host + * calling its own send path builds the same envelope a client would, fingerprint included. */ +export function structuredAgentSessionSendMutation( entry: StructuredAgentSessionOutboxEntry, expectedRuntimeFence: number -): Record { +): StructuredAgentSessionSendMutation { const fields = { body: entry.body } return { envelope: { @@ -213,6 +223,13 @@ export function structuredAgentSessionSendRequest( } } +export function structuredAgentSessionSendRequest( + entry: StructuredAgentSessionOutboxEntry, + expectedRuntimeFence: number +): Record { + return structuredAgentSessionSendMutation(entry, expectedRuntimeFence) +} + export type StructuredAgentSessionSendFailure = 'delivery-unknown' | 'failed' export function classifyStructuredAgentSessionSendFailure( diff --git a/src/shared/structured-native-chat-launch-route.ts b/src/shared/structured-native-chat-launch-route.ts index bde8de06c0a..5ae855ea47e 100644 --- a/src/shared/structured-native-chat-launch-route.ts +++ b/src/shared/structured-native-chat-launch-route.ts @@ -13,6 +13,7 @@ import type { GlobalSettings } from './global-settings-types' import type { ProjectExecutionRuntimeResolution } from './project-execution-runtime' import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from './protocol-version' import type { TuiAgent } from './tui-agent' +import type { WorkspaceLaunchKind } from './workspace-launch-kind' export type NativeChatDefaultSettings = Pick< GlobalSettings, @@ -44,7 +45,8 @@ export type StructuredNativeChatSupportInput = { executionHostId: string /** Capabilities of the host this launch would run on. `null` = not yet established. */ hostCapabilities: readonly string[] | null - workspaceKind?: 'git-worktree' | 'folder' | 'floating' + /** Host-derived. Absent means the kind was never established, which is not evidence of any kind. */ + workspaceKind?: WorkspaceLaunchKind projectRuntime?: ProjectExecutionRuntimeResolution | null requiresTuiLaunchCommand?: boolean /** An existing PTY agent keeps its execution transport. */ 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/terminal-snapshot-unavailability.ts b/src/shared/terminal-snapshot-unavailability.ts index 6d3ac26774f..3f5f73962ff 100644 --- a/src/shared/terminal-snapshot-unavailability.ts +++ b/src/shared/terminal-snapshot-unavailability.ts @@ -4,8 +4,13 @@ * Sent as the additive `unavailable` field on the SnapshotStart frame. Hosts that predate * this field omit it, so an absent value means "the host did not say" — never "nothing exists". * Both current reasons are transient: the host could not answer *now*, not that the pane has - * no retained output. A pane that genuinely has nothing still comes back as a real snapshot - * whose `data` is empty, because the host successfully serialized and found it empty. + * no retained output. + * + * A real snapshot with empty `data` and no reason is NOT proof the pane is empty either: + * `serializeTerminalBufferFromAvailableState` returns the renderer serializer's un-hydrated + * shell (`data: ''`) when a parked desktop pane registered its serializer before its xterm + * mounted and no provider history exists. Nothing on this wire positively says "the host + * retains nothing"; readers must treat an imageless success as unverifiable, never as empty. */ export const TERMINAL_SNAPSHOT_UNAVAILABLE_REASONS = [ // The pending-output buffer overflowed twice while serializing, so the reply was truncated to nothing. diff --git a/src/shared/workspace-launch-kind.ts b/src/shared/workspace-launch-kind.ts new file mode 100644 index 00000000000..b4c03ef96dc --- /dev/null +++ b/src/shared/workspace-launch-kind.ts @@ -0,0 +1,23 @@ +/** + * Which kind of workspace a launch lands in, read from the workspace's own id. + * + * The three kinds are not interchangeable to a launch: only a git worktree and a folder workspace + * have somewhere a structured session can live, and the floating terminal — a sentinel with no + * backing repo, worktree or folder row — can host a PTY and nothing else. + * + * It lives in `shared` because both sides of the launch ask the same question: the renderer when a + * user opens an agent tab, and the host when it resolves an `agent.launch` target. A host must + * never take the answer from a caller, so it derives it here from the id it resolved itself. + */ + +import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' +import { parseWorkspaceKey } from './workspace-scope' + +export type WorkspaceLaunchKind = 'git-worktree' | 'folder' | 'floating' + +export function workspaceKindForWorktreeId(worktreeId: string): WorkspaceLaunchKind { + if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) { + return 'floating' + } + return parseWorkspaceKey(worktreeId)?.type === 'folder' ? 'folder' : 'git-worktree' +} 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): { 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 } diff --git a/src/shared/zod-salvage.ts b/src/shared/zod-salvage.ts index 6a05550c45a..2c64c33b770 100644 --- a/src/shared/zod-salvage.ts +++ b/src/shared/zod-salvage.ts @@ -86,13 +86,32 @@ function recordEntryKeys(raw: unknown): string[] | null { * instead of rejecting the reply. A non-string stays fatal, so this widens the vocabulary without * also accepting the wrong type. Not `.catch()`, which would swallow absence too. */ -export function openEnum( +// `readonly string[]` rather than a non-empty tuple so a hostUnionArms list can feed it; z.enum +// takes the same, so the tuple constraint only excluded callers zod itself accepts. `const` keeps a +// bare literal's arms, which the parsed type is built from. +export function openEnum( values: T, fallback: F ): z.ZodType { return z.enum(values).or(z.string().transform(() => fallback)) } +/** + * The arms of a closed enum, spelled as a coverage record over the host's own union so tsc holds + * the schema to that union both ways: an arm the host adds is a missing property here, one it drops + * is an excess property. `NoInfer` keeps U from being read off the record, so omitting the type + * argument leaves it at its `never` default and the parameter becomes `never` — the call fails to + * compile rather than pinning the record only to itself. It belongs in the schema module rather + * than its test because the list is what feeds `z.enum`: the coverage record and the arms the + * schema actually accepts are then one object, and a test-side copy could drift from it. + */ +export function hostUnionArms( + coverage: [U] extends [never] ? never : Readonly, true>> +): readonly U[] { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mapped parameter type makes every key exactly a U; Object.keys only loses that at the type level. + return Object.keys(coverage) as U[] +} + /** Array that drops the elements it cannot parse instead of failing. * Absence stays fatal on its own: both containers issue on `undefined` and, being bare transforms, * set neither optin nor optout, and zod only swallows an absent key's issues when a field is both. 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) } } 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() + } +}) 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 diff --git a/tests/e2e/runtime-graph-publication-probe.ts b/tests/e2e/runtime-graph-publication-probe.ts new file mode 100644 index 00000000000..b61ca666a3a --- /dev/null +++ b/tests/e2e/runtime-graph-publication-probe.ts @@ -0,0 +1,280 @@ +/** + * Diagnostic-only probe for renderer runtime-graph publication cost. + * + * `window.api` is frozen by contextBridge, so the renderer cannot wrap + * `runtime.syncWindowGraph`. Instead this counts publications where they land — + * main's `runtime:syncWindowGraph` invoke handler. + * + * `publications` and `mainHandler` are trustworthy. The long-task fields are NOT + * yet: on 2026-09-16 an injected 250 ms renderer busy-wait produced zero entries + * even though `longtask` is in `supportedEntryTypes`, so a zero there means + * "oracle unproven", not "no long task happened". Run with + * ORCA_TYPING_BENCH_GRAPH_PROBE_SELFTEST_MS and require a non-zero + * `selfTestLongTaskMs` before believing any long-task number. That number comes + * from the entry the busy-wait actually ran in (see `partitionSelfTestLongTask`), + * so unrelated work cannot satisfy it, and the same entry is withheld from every + * workload summary so the oracle does not measure itself. + * + * Renderer-side per-publication build time is unavailable here; attribute it + * with a separate --cpu-profile run instead. + * + * Keep this out of acceptance timing runs (gate: ORCA_TYPING_BENCH_GRAPH_PROBE=1). + */ +import type { ElectronApplication, Page } from '@stablyai/playwright-test' + +const GRAPH_CHANNEL = 'runtime:syncWindowGraph' + +export type DurationSummary = { + count: number + totalMs: number + maxMs: number + p50Ms: number + p90Ms: number +} + +export type LongTaskSample = { startEpochMs: number; durationMs: number } + +/** Renderer-clock bounds of the injected busy-wait, same base as long-task entries. */ +export type RendererLongTaskSelfTestWindow = { startEpochMs: number; endEpochMs: number } + +export type RuntimeGraphPublicationProbeSnapshot = { + mainCounterInstalled: boolean + mainCounterReason: string + rendererObserverInstalled: boolean + rendererObserverReason: string + /** Publications counted at main's invoke handler. */ + publications: number + /** Main-side handler duration (excludes the renderer-side graph build). */ + mainHandler: DurationSummary + /** Gaps between consecutive publications, epoch ms. */ + publicationIntervalMs: DurationSummary + /** Workload long tasks; the self-test's own entry is excluded. */ + longTasks: DurationSummary + /** Non-zero only when the self-test's own entry was observed; proves the oracle is live. */ + selfTestLongTaskMs: number + /** Long tasks whose window contains a publication's main-side arrival. */ + longTasksAroundPublication: DurationSummary + longestLongTasks: LongTaskSample[] +} + +type MainProbeGlobals = { + __orcaGraphPublicationMainProbe?: { + stop: () => { count: number; handlerMs: number[]; atEpochMs: number[] } + } +} + +type RendererProbeWindow = Window & { + __orcaGraphPublicationRendererProbe?: { + stop: () => { timeOrigin: number; longTasks: { start: number; duration: number }[] } + } +} + +function summarize(values: number[]): DurationSummary { + if (values.length === 0) { + return { count: 0, totalMs: 0, maxMs: 0, p50Ms: 0, p90Ms: 0 } + } + const sorted = [...values].sort((a, b) => a - b) + const at = (fraction: number): number => + sorted[Math.min(sorted.length - 1, Math.floor(fraction * sorted.length))] ?? 0 + const round = (value: number): number => Number(value.toFixed(1)) + return { + count: sorted.length, + totalMs: round(sorted.reduce((sum, value) => sum + value, 0)), + maxMs: round(sorted.at(-1) ?? 0), + p50Ms: round(at(0.5)), + p90Ms: round(at(0.9)) + } +} + +/** + * Presence precondition for the long-task oracle: burns a known span on the + * renderer thread so a run that reports zero long tasks has proved it could + * have seen one. Returns the busy-wait's own bounds — a cutoff timestamp would + * let any earlier unrelated long task stand in for it. + */ +export async function injectRendererLongTaskSelfTest( + page: Page, + busyMs: number +): Promise { + return page.evaluate((durationMs) => { + const startedAt = performance.now() + const deadline = startedAt + durationMs + while (performance.now() < deadline) { + // Intentional busy wait: setTimeout would not produce a long task. + } + return { + startEpochMs: performance.timeOrigin + startedAt, + endEpochMs: performance.timeOrigin + performance.now() + } + }, busyMs) +} + +/** + * Main-thread tasks never overlap, so at most one long task can contain the + * busy-wait's midpoint and that one is the task the busy-wait ran in. Anything + * else — including a long task that merely started earlier — leaves the oracle + * unproven rather than falsely satisfied. + */ +export function partitionSelfTestLongTask( + longTasks: LongTaskSample[], + selfTest: RendererLongTaskSelfTestWindow | null +): { selfTestLongTaskMs: number; workloadLongTasks: LongTaskSample[] } { + if (!selfTest) { + return { selfTestLongTaskMs: 0, workloadLongTasks: longTasks } + } + const midpoint = (selfTest.startEpochMs + selfTest.endEpochMs) / 2 + const selfTestTask = longTasks.find( + (task) => task.startEpochMs <= midpoint && midpoint <= task.startEpochMs + task.durationMs + ) + if (!selfTestTask) { + return { selfTestLongTaskMs: 0, workloadLongTasks: longTasks } + } + return { + selfTestLongTaskMs: Number(selfTestTask.durationMs.toFixed(1)), + // Identity, not value: duplicate-looking entries must not be dropped too. + workloadLongTasks: longTasks.filter((task) => task !== selfTestTask) + } +} + +export async function startRuntimeGraphPublicationProbe( + electronApp: ElectronApplication, + page: Page +): Promise<{ main: string; renderer: string }> { + const main = await electronApp.evaluate(({ ipcMain }, channel): string => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: diagnostic-only read of Electron's private invoke-handler map; every use is guarded by the shape checks below. + const registry = (ipcMain as unknown as { _invokeHandlers?: Map }) + ._invokeHandlers + if (!(registry instanceof Map)) { + return 'no-invoke-handler-registry' + } + const original = registry.get(channel) + if (typeof original !== 'function') { + return `handler-missing typeof=${typeof original}` + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Electron stores invoke handlers as callables; arguments are forwarded unchanged and never introspected. + const call = original as (...args: unknown[]) => unknown + const handlerMs: number[] = [] + const atEpochMs: number[] = [] + const publications = { count: 0, handlerMs, atEpochMs } + const wrapped = async (...args: unknown[]): Promise => { + const startedAt = Date.now() + const startedHr = process.hrtime.bigint() + publications.count += 1 + publications.atEpochMs.push(startedAt) + try { + return await call(...args) + } finally { + publications.handlerMs.push(Number(process.hrtime.bigint() - startedHr) / 1e6) + } + } + registry.set(channel, wrapped) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: main-process bag read back only by the paired stop() call in this same run. + const globals = globalThis as unknown as MainProbeGlobals + globals.__orcaGraphPublicationMainProbe = { + stop: () => { + if (registry.get(channel) === wrapped) { + registry.set(channel, original) + } + delete globals.__orcaGraphPublicationMainProbe + return publications + } + } + return 'installed' + }, GRAPH_CHANNEL) + + const renderer = await page.evaluate((): string => { + const probeWindow: RendererProbeWindow = window + if (probeWindow.__orcaGraphPublicationRendererProbe) { + return 'already-installed' + } + const supported = PerformanceObserver.supportedEntryTypes ?? [] + if (!supported.includes('longtask')) { + return `longtask-unsupported supported=${supported.join('|')}` + } + const longTasks: { start: number; duration: number }[] = [] + let observer: PerformanceObserver + try { + observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + longTasks.push({ start: entry.startTime, duration: entry.duration }) + } + }) + observer.observe({ entryTypes: ['longtask'] }) + } catch (error) { + return `longtask-observer-unavailable ${String(error)}` + } + probeWindow.__orcaGraphPublicationRendererProbe = { + stop: () => { + for (const entry of observer.takeRecords()) { + longTasks.push({ start: entry.startTime, duration: entry.duration }) + } + observer.disconnect() + delete probeWindow.__orcaGraphPublicationRendererProbe + return { timeOrigin: performance.timeOrigin, longTasks } + } + } + return 'installed' + }) + + return { main, renderer } +} + +export async function stopRuntimeGraphPublicationProbe( + electronApp: ElectronApplication, + page: Page, + start: { main: string; renderer: string }, + selfTest: RendererLongTaskSelfTestWindow | null = null +): Promise { + const mainResult = + start.main === 'installed' + ? await electronApp.evaluate(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: reads back the bag installed by startRuntimeGraphPublicationProbe in this run. + const globals = globalThis as unknown as MainProbeGlobals + return globals.__orcaGraphPublicationMainProbe?.stop() ?? null + }) + : null + const rendererResult = + start.renderer === 'installed' + ? await page.evaluate(() => { + const probeWindow: RendererProbeWindow = window + return probeWindow.__orcaGraphPublicationRendererProbe?.stop() ?? null + }) + : null + + const publicationEpochMs = mainResult?.atEpochMs ?? [] + const intervals = publicationEpochMs + .slice(1) + .map((value, index) => value - (publicationEpochMs[index] ?? value)) + const timeOrigin = rendererResult?.timeOrigin ?? 0 + const longTasks: LongTaskSample[] = (rendererResult?.longTasks ?? []).map((task) => ({ + startEpochMs: timeOrigin + task.start, + durationMs: task.duration + })) + const { selfTestLongTaskMs, workloadLongTasks } = partitionSelfTestLongTask(longTasks, selfTest) + // A renderer graph build ends at the invoke; allow slack for IPC transit either way. + const around = workloadLongTasks.filter((task) => + publicationEpochMs.some( + (at) => at >= task.startEpochMs - 5 && at <= task.startEpochMs + task.durationMs + 50 + ) + ) + + return { + mainCounterInstalled: start.main === 'installed', + mainCounterReason: start.main, + rendererObserverInstalled: start.renderer === 'installed', + rendererObserverReason: start.renderer, + publications: mainResult?.count ?? 0, + mainHandler: summarize(mainResult?.handlerMs ?? []), + publicationIntervalMs: summarize(intervals), + longTasks: summarize(workloadLongTasks.map((task) => task.durationMs)), + selfTestLongTaskMs, + longTasksAroundPublication: summarize(around.map((task) => task.durationMs)), + longestLongTasks: [...workloadLongTasks] + .sort((a, b) => b.durationMs - a.durationMs) + .slice(0, 10) + .map((task) => ({ + startEpochMs: task.startEpochMs, + durationMs: Number(task.durationMs.toFixed(1)) + })) + } +} diff --git a/tests/e2e/runtime-graph-publication-probe.unit.test.ts b/tests/e2e/runtime-graph-publication-probe.unit.test.ts new file mode 100644 index 00000000000..c3f46fb5a00 --- /dev/null +++ b/tests/e2e/runtime-graph-publication-probe.unit.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' +import { partitionSelfTestLongTask, type LongTaskSample } from './runtime-graph-publication-probe' + +// The observer is live from probe start, so setup work lands in the array before +// the injected busy-wait. Index 0 is the decoy a start-time cutoff would accept. +const setupTask: LongTaskSample = { startEpochMs: 1_000, durationMs: 120 } +const selfTestTask: LongTaskSample = { startEpochMs: 5_000, durationMs: 260 } +const workloadTask: LongTaskSample = { startEpochMs: 9_000, durationMs: 80 } + +const selfTestWindow = { startEpochMs: 5_005, endEpochMs: 5_255 } + +describe('partitionSelfTestLongTask', () => { + it('attributes the entry the busy-wait ran in, not an earlier one', () => { + const result = partitionSelfTestLongTask( + [setupTask, selfTestTask, workloadTask], + selfTestWindow + ) + expect(result.selfTestLongTaskMs).toBe(260) + expect(result.workloadLongTasks).toEqual([setupTask, workloadTask]) + }) + + it('leaves the oracle unproven when only unrelated tasks were observed', () => { + const result = partitionSelfTestLongTask([setupTask, workloadTask], selfTestWindow) + expect(result.selfTestLongTaskMs).toBe(0) + expect(result.workloadLongTasks).toEqual([setupTask, workloadTask]) + }) + + it('leaves the oracle unproven when a task ends before the busy-wait starts', () => { + // Touches the window's lower edge but does not reach its midpoint. + const result = partitionSelfTestLongTask([{ startEpochMs: 4_900, durationMs: 110 }], { + startEpochMs: 5_000, + endEpochMs: 5_250 + }) + expect(result.selfTestLongTaskMs).toBe(0) + }) + + it('keeps every task when no self-test ran', () => { + const result = partitionSelfTestLongTask([setupTask, workloadTask], null) + expect(result.selfTestLongTaskMs).toBe(0) + expect(result.workloadLongTasks).toEqual([setupTask, workloadTask]) + }) + + it('removes only the matched entry when another has identical values', () => { + const twin: LongTaskSample = { ...selfTestTask } + const result = partitionSelfTestLongTask([selfTestTask, twin], selfTestWindow) + expect(result.workloadLongTasks).toHaveLength(1) + expect(result.workloadLongTasks[0]).toBe(twin) + }) + + it('rounds the reported duration to one decimal', () => { + const result = partitionSelfTestLongTask( + [{ startEpochMs: 5_000, durationMs: 251.2649 }], + selfTestWindow + ) + expect(result.selfTestLongTaskMs).toBe(251.3) + }) +}) 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) + } + }) +}) diff --git a/tests/e2e/terminal-multi-workspace-typing-latency.spec.ts b/tests/e2e/terminal-multi-workspace-typing-latency.spec.ts index 624ddb1077d..61e5d26a20f 100644 --- a/tests/e2e/terminal-multi-workspace-typing-latency.spec.ts +++ b/tests/e2e/terminal-multi-workspace-typing-latency.spec.ts @@ -83,6 +83,13 @@ import { startAccumulatedTitleTraffic, stopAccumulatedTitleTraffic } from './accumulated-workspace-title-fixture' +import { + injectRendererLongTaskSelfTest, + startRuntimeGraphPublicationProbe, + stopRuntimeGraphPublicationProbe, + type RendererLongTaskSelfTestWindow, + type RuntimeGraphPublicationProbeSnapshot +} from './runtime-graph-publication-probe' const BENCH_ENABLED = process.env.ORCA_TYPING_BENCH === '1' @@ -101,6 +108,12 @@ const PTY_METADATA = process.env.ORCA_TYPING_BENCH_PTY_METADATA === '1' const BENCH_LABEL = process.env.ORCA_TYPING_BENCH_LABEL ?? 'dev' // Request optional probes by default; the report records when the build does not install them. const BENCH_INSTRUMENTATION_REQUESTED = process.env.ORCA_TYPING_BENCH_INSTRUMENTATION !== '0' +// Diagnostic only: patching main's invoke handler is observer overhead, so keep it out of acceptance runs. +const GRAPH_PROBE_REQUESTED = process.env.ORCA_TYPING_BENCH_GRAPH_PROBE === '1' +const GRAPH_PROBE_SELF_TEST_MS = readPositiveInt('ORCA_TYPING_BENCH_GRAPH_PROBE_SELFTEST_MS', 0) +// Estimates a slower single core. Applied only around the typing window: throttling setup would +// change what the fixture manages to build, not just how fast the measured window runs. +const CPU_THROTTLE_RATE = readPositiveInt('ORCA_TYPING_BENCH_CPU_THROTTLE', 1) // Load must outlive setup (pane splits, worktree switches) plus the typing // window; generously padded because setup time varies with pane count. @@ -156,10 +169,55 @@ function spawnCpuPressureWorkers(): ChildProcess[] { ) } +/** Rate 1 is a no-op, so an unthrottled run opens no CDP session at all. */ +async function withRendererCpuThrottle( + page: Page, + rate: number, + run: () => Promise +): Promise<{ result: T; appliedRate: number }> { + if (rate <= 1) { + return { result: await run(), appliedRate: 1 } + } + const session = await page.context().newCDPSession(page) + try { + await session.send('Emulation.setCPUThrottlingRate', { rate }) + return { result: await run(), appliedRate: rate } + } finally { + await session.send('Emulation.setCPUThrottlingRate', { rate: 1 }).catch(() => {}) + await session.detach().catch(() => {}) + } +} + +/** Carries the conditions the window ran under, so the report cannot invent them. */ +type TypingWindowMeasurement = { + measurement: PacedTypingMeasurement + appliedCpuThrottleRate: number +} + +/** + * The only way to obtain a measurement writeBenchReport will accept: a scenario + * that skips the throttle cannot then report one. + */ +async function measureTypingWindow( + page: Page, + runId: string, + sidecarPath: string +): Promise { + const { result, appliedRate } = await withRendererCpuThrottle(page, CPU_THROTTLE_RATE, () => + withTypingRendererCpuProfile(page, process.env.ORCA_TYPING_BENCH_CPU_PROFILE, () => + measurePacedTyping(page, runId, sidecarPath, { + keyCount: KEY_COUNT, + keyCadenceMs: KEY_CADENCE_MS + }) + ) + ) + return { measurement: result, appliedCpuThrottleRate: appliedRate } +} + function writeBenchReport( testInfo: TestInfo, scenario: string, - measurement: PacedTypingMeasurement, + measured: TypingWindowMeasurement, scheduler: SchedulerDebugSnapshot | null, mainDelivery: MainDeliveryDebugSnapshot | null, instrumentation?: unknown, @@ -168,8 +226,10 @@ function writeBenchReport( statusIngressValidation?: AccumulatedStatusIngressValidation | null, scaleCensus?: unknown, accumulatedFixture?: unknown, - ptyWorkload?: unknown + ptyWorkload?: unknown, + graphProbe?: RuntimeGraphPublicationProbeSnapshot | null ): void { + const { measurement, appliedCpuThrottleRate } = measured const report = { benchmark: 'multi-workspace-typing-latency', label: BENCH_LABEL, @@ -208,6 +268,9 @@ function writeBenchReport( 100 ), instrumentationRequested: BENCH_INSTRUMENTATION_REQUESTED, + graphProbeRequested: GRAPH_PROBE_REQUESTED, + // What this scenario actually ran under, not what the flag requested. + cpuThrottleRate: appliedCpuThrottleRate, statusTrafficModel: PTY_METADATA ? 'pty-osc-through-runtime-and-ipc-bridge' : 'electron-ipc-burst-through-production-bridge' @@ -221,7 +284,8 @@ function writeBenchReport( statusIngressValidation: statusIngressValidation ?? null, scaleCensus: scaleCensus ?? null, accumulatedFixture: accumulatedFixture ?? null, - ptyWorkload: ptyWorkload ?? null + ptyWorkload: ptyWorkload ?? null, + graphProbe: graphProbe ?? null } mkdirSync(RESULTS_DIR, { recursive: true }) const stamp = report.timestamp.replace(/[:.]/g, '-') @@ -320,19 +384,12 @@ test.describe('Multi-workspace sustained typing latency bench', () => { try { await resetDeliveryDebug(orcaPage) await startTypingProbe(orcaPage, typingPtyId, probePath, runId) - const measurement = await withTypingRendererCpuProfile( - orcaPage, - process.env.ORCA_TYPING_BENCH_CPU_PROFILE, - () => - measurePacedTyping(orcaPage, runId, sidecarPath, { - keyCount: KEY_COUNT, - keyCadenceMs: KEY_CADENCE_MS - }) - ) + const measured = await measureTypingWindow(orcaPage, runId, sidecarPath) + const { measurement } = measured writeBenchReport( testInfo, 'baseline', - measurement, + measured, await readSchedulerDebug(orcaPage), await readMainDeliveryDebug(orcaPage) ) @@ -372,6 +429,8 @@ test.describe('Multi-workspace sustained typing latency bench', () => { let titleWorkload: { registeredTabs: number; registeredPanes: number } | null = null let statusTrafficStarted = false let instrumentationAvailable = false + let graphProbeStart: { main: string; renderer: string } | null = null + let graphProbeSelfTest: RendererLongTaskSelfTestWindow | null = null let statusIngressValidation: AccumulatedStatusIngressValidation | null = null try { await switchToWorktree(orcaPage, loadWorktreeId) @@ -425,6 +484,16 @@ test.describe('Multi-workspace sustained typing latency bench', () => { if (BENCH_INSTRUMENTATION_REQUESTED) { instrumentationAvailable = await startAccumulatedBenchmarkInstrumentation(orcaPage) } + if (GRAPH_PROBE_REQUESTED) { + graphProbeStart = await startRuntimeGraphPublicationProbe(electronApp, orcaPage) + if (GRAPH_PROBE_SELF_TEST_MS > 0) { + graphProbeSelfTest = await injectRendererLongTaskSelfTest( + orcaPage, + GRAPH_PROBE_SELF_TEST_MS + ) + } + console.log(`[multi-workspace-typing] graph probe: ${JSON.stringify(graphProbeStart)}`) + } if (statusTrafficEnabled) { const statusTraffic = await startAccumulatedStatusTraffic( electronApp, @@ -466,15 +535,8 @@ test.describe('Multi-workspace sustained typing latency bench', () => { .toBe(LOAD_PANES) } await startTypingProbe(orcaPage, typingPtyId, probePath, runId) - const measurement = await withTypingRendererCpuProfile( - orcaPage, - process.env.ORCA_TYPING_BENCH_CPU_PROFILE, - () => - measurePacedTyping(orcaPage, runId, sidecarPath, { - keyCount: KEY_COUNT, - keyCadenceMs: KEY_CADENCE_MS - }) - ) + const measured = await measureTypingWindow(orcaPage, runId, sidecarPath) + const { measurement } = measured const statusWorkload = statusTrafficStarted ? await stopAccumulatedStatusTraffic(electronApp, orcaPage) : null @@ -491,10 +553,19 @@ test.describe('Multi-workspace sustained typing latency bench', () => { ? await stopAccumulatedBenchmarkInstrumentation(orcaPage) : { available: false as const, reason: 'disabled' as const, snapshot: null } instrumentationAvailable = false + const graphProbe = graphProbeStart + ? await stopRuntimeGraphPublicationProbe( + electronApp, + orcaPage, + graphProbeStart, + graphProbeSelfTest + ) + : null + graphProbeStart = null writeBenchReport( testInfo, `hidden-load-${LOAD_PANES}x${LOAD_RATE_KBPS}kbps-cpu${CPU_WORKERS}`, - measurement, + measured, await readSchedulerDebug(orcaPage), await readMainDeliveryDebug(orcaPage), instrumentation, @@ -527,7 +598,8 @@ test.describe('Multi-workspace sustained typing latency bench', () => { ).length } }) - } + }, + graphProbe ) const screenDirectory = path.resolve('.tmp', 'typing-reproduction') mkdirSync(screenDirectory, { recursive: true }) @@ -542,6 +614,14 @@ test.describe('Multi-workspace sustained typing latency bench', () => { if (instrumentationAvailable) { await stopAccumulatedBenchmarkInstrumentation(orcaPage).catch(() => undefined) } + if (graphProbeStart) { + await stopRuntimeGraphPublicationProbe( + electronApp, + orcaPage, + graphProbeStart, + graphProbeSelfTest + ).catch(() => undefined) + } if (statusTrafficStarted) { await stopAccumulatedStatusTraffic(electronApp, orcaPage) } @@ -593,19 +673,12 @@ test.describe('Multi-workspace sustained typing latency bench', () => { await resetDeliveryDebug(orcaPage) await startTypingProbe(orcaPage, typingPane.ptyId, probePath, runId) - const measurement = await withTypingRendererCpuProfile( - orcaPage, - process.env.ORCA_TYPING_BENCH_CPU_PROFILE, - () => - measurePacedTyping(orcaPage, runId, sidecarPath, { - keyCount: KEY_COUNT, - keyCadenceMs: KEY_CADENCE_MS - }) - ) + const measured = await measureTypingWindow(orcaPage, runId, sidecarPath) + const { measurement } = measured writeBenchReport( testInfo, `visible-split-${LOAD_RATE_KBPS}kbps-cpu${CPU_WORKERS}`, - measurement, + measured, await readSchedulerDebug(orcaPage), await readMainDeliveryDebug(orcaPage) ) 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 +) 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 +)