diff --git a/.github/workflows/bun-profile-tests.yml b/.github/workflows/bun-profile-tests.yml index 5fae18f2f84..1a292292510 100644 --- a/.github/workflows/bun-profile-tests.yml +++ b/.github/workflows/bun-profile-tests.yml @@ -17,6 +17,24 @@ on: - '.pnpmfile.cjs' - '.github/actions/install-node-dependencies/**' - '.github/workflows/bun-profile-tests.yml' + # The pull request qualifies one platform for an unflavoured change; this is where all six + # are re-qualified, so a platform break surfaces minutes after merge instead of next cron. + push: + branches: [main] + paths: + - 'src/**' + - 'config/**' + - 'native/**' + - 'tests/**' + - 'resources/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - 'tsconfig.json' + - '.npmrc' + - '.pnpmfile.cjs' + - '.github/actions/install-node-dependencies/**' + - '.github/workflows/bun-profile-tests.yml' workflow_dispatch: schedule: - cron: '30 11 * * *' @@ -57,7 +75,11 @@ jobs: persistence: needs: changes # Missing/failed detection runs the full matrix; manual runs remain unconditional. - if: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' }} + # A draft carries no platform verdict; readiness re-triggers this workflow. Spelled against + # the event name so the push and schedule paths do not rest on a null property comparison. + if: >- + ${{ !cancelled() && needs.changes.outputs.should_run != 'false' && + (github.event_name != 'pull_request' || github.event.pull_request.draft != true) }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/homebrew-bump.yml b/.github/workflows/homebrew-bump.yml index 9b04d3205fc..a5f9b352f6e 100644 --- a/.github/workflows/homebrew-bump.yml +++ b/.github/workflows/homebrew-bump.yml @@ -132,11 +132,10 @@ jobs: print(src) PY - # Why: reuse the existing buf0-bot GitHub App (also used by - # track-community-prs.yaml) instead of minting a new PAT. The app is - # installed org-wide, so it has access to stablyai/homebrew-orca - # automatically. GITHUB_TOKEN cannot push cross-repo; a short-lived - # installation token can. + # Why: reuse the existing buf0-bot GitHub App instead of minting a new + # PAT. The app is installed org-wide, so it has access to + # stablyai/homebrew-orca automatically. GITHUB_TOKEN cannot push + # cross-repo; a short-lived installation token can. - name: Generate buf0-bot token id: app-token uses: actions/create-github-app-token@v3 diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 7ec322b0203..bc9f8241f94 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -41,9 +41,8 @@ on: - 'config/scripts/mobile-test-change-scope*' - 'config/scripts/pr-code-change-scope.mjs' - 'config/scripts/mobile-recording-pin-checkout.test.mjs' - # Why main too: a behaviour-change branch legitimately pins its own last fenced commit, and that - # commit only stops being reachable when the branch squash-merges. The pull_request run cannot - # see that; this one is where the pin guard finds it. + # Why main too: a squash is where a spliced corpus lands, and where a behaviour-change branch's + # own pinned commit leaves main's history for its pull request's head ref, which the guard follows. push: branches: - main @@ -182,6 +181,10 @@ jobs: name: RPC recording pin # Why ARM: pure Node plus git; the golden comparison masks `platform`. runs-on: ubuntu-24.04-arm + # Why pull-requests: the guard asks GitHub which pull requests hold a pin main's history lacks. + permissions: + contents: read + pull-requests: read defaults: run: @@ -191,7 +194,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: - # The ancestry verdict is read straight off history. On a shallow checkout + # The reachability verdict is read straight off history. On a shallow checkout # `git merge-base --is-ancestor` answers from grafted parents, so the guard refuses to # answer at all rather than reporting a pass it has no evidence for -- and the pinned tree # below has to be checkable out. @@ -211,12 +214,14 @@ jobs: # Seconds. No `--ref`, so the pin is judged against the same tree it was read out of. On a # pull request that is the merge preview, which already carries main's repins; judging the # branch head instead fails every branch cut before the day's repin, and its instruction would - # tell the author to pin their own head -- creating the break this guard exists to catch. A - # branch that pins its own commit passes here and fails on the push after the squash, which is - # where the pin actually leaves the history. + # tell the author to pin their own head. A branch that pins its own commit still passes on the + # push after the squash: the guard asks GitHub which pull requests hold the pin and fetches + # their `refs/pull//head`, which GitHub keeps for good. The token lifts the API rate limit. - name: Check the recording pin is reachable shell: bash - run: pnpm exec tsx scripts/rpc-recording-pin-guard.mts ancestry + env: + GITHUB_TOKEN: ${{ github.token }} + run: pnpm exec tsx scripts/rpc-recording-pin-guard.mts reachable # ~2 min locally for the record itself, so it is gated rather than run twice over. A pull # request that moves none of the corpus, the manifest or the recorder cannot move this @@ -226,6 +231,7 @@ jobs: - name: Reproduce the corpus from the pinned tree shell: bash env: + GITHUB_TOKEN: ${{ github.token }} PIN_GUARD_BASE: ${{ github.event.pull_request.base.sha }} run: | if [ -n "$PIN_GUARD_BASE" ]; then diff --git a/.github/workflows/node-next-compat.yml b/.github/workflows/node-next-compat.yml index cb0d303f8c5..fc9b9cedaf9 100644 --- a/.github/workflows/node-next-compat.yml +++ b/.github/workflows/node-next-compat.yml @@ -34,8 +34,12 @@ jobs: native-runtime: node node-version: ${{ matrix.node }} + unit_plan: + uses: ./.github/workflows/unit-plan.yml + test: - needs: [test_native_cache] + needs: [test_native_cache, unit_plan] uses: ./.github/workflows/unit-tests.yml with: node_versions: '["24", "26"]' + shards: ${{ needs.unit_plan.outputs.shards }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ba3ba7556ed..795beca131e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -640,12 +640,24 @@ jobs: native-runtime: node node-version: '24' + # Why hoisted out of unit-tests.yml: a caller's `needs` gate the whole called workflow, so + # while planning lived in there it queued behind static analysis and typecheck and the shards + # then queued behind it. Planning needs none of their output, so running it against + # code_paths alone overlaps it with the gate. Measured a median 93s off the matrix's start. + unit_plan: + needs: [code_paths] + if: needs.code_paths.outputs.test == 'true' + uses: ./.github/workflows/unit-plan.yml + with: + selection_mode: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }} + test: - needs: [code_paths, test_native_cache, static_analysis, typecheck] + needs: [code_paths, unit_plan, test_native_cache, static_analysis, typecheck] # Honor cancellation while allowing the optional native-cache primer to skip. if: >- !cancelled() && needs.code_paths.outputs.test == 'true' && + needs.unit_plan.result == 'success' && needs.static_analysis.result == 'success' && needs.typecheck.result == 'success' && (needs.test_native_cache.result == 'success' || needs.test_native_cache.result == 'skipped') @@ -653,7 +665,15 @@ jobs: with: node_versions: '["24"]' runner: ubuntu-24.04-arm - selection_mode: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }} + shards: ${{ needs.unit_plan.outputs.shards }} + + # Why a sibling and not part of the test workflow: it is advisory, so it must not delay the + # gate. Inside unit-tests.yml a caller's `needs: test` waited for it, holding verify ~36s + # after the last shard. Deliberately absent from verify's needs for the same reason. + unit_selection_evidence: + needs: [test] + if: ${{ !cancelled() && needs.test.result == 'success' }} + uses: ./.github/workflows/unit-selection-evidence.yml # Why a separate job: the test needs a real Chrome, and the sharded `test` matrix # would pay for it on every shard to run one file in whichever shard it landed in. diff --git a/.github/workflows/track-community-prs.yaml b/.github/workflows/track-community-prs.yaml deleted file mode 100644 index 7a588663fb2..00000000000 --- a/.github/workflows/track-community-prs.yaml +++ /dev/null @@ -1,120 +0,0 @@ -name: Track Community PRs - -on: - pull_request_target: - types: [opened, reopened, ready_for_review] - workflow_dispatch: - inputs: - pr_number: - description: 'PR number to backfill or retry' - required: true - type: number - -permissions: - contents: read - -jobs: - track-community-pr: - # API-only bookkeeping fits the free single-CPU container runner. - runs-on: ubuntu-slim - timeout-minutes: 5 - steps: - - name: Generate bufo-bot token - id: app-token - uses: actions/create-github-app-token@v3 - with: - app-id: 2590194 - private-key: ${{ secrets.BUFO_BOT_PRIVATE_KEY }} - owner: stablyai - - - name: Add PR to project - uses: actions/github-script@v8 - env: - PROJECT_OWNER: stablyai - PROJECT_NUMBER: '13' - INTERNAL_TEAM_SLUG: stably-eng - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const projectOwner = process.env.PROJECT_OWNER; - const projectNumber = Number(process.env.PROJECT_NUMBER); - const internalTeamSlug = process.env.INTERNAL_TEAM_SLUG; - const owner = context.repo.owner; - const repo = context.repo.repo; - - const prNumber = context.eventName === 'workflow_dispatch' - ? Number(context.payload.inputs.pr_number) - : context.payload.pull_request.number; - - const { data: pr } = await github.rest.pulls.get({ - owner, - repo, - pull_number: prNumber, - }); - - const author = pr.user.login; - - const skippedAuthors = new Set([ - 'github-actions[bot]', - 'dependabot[bot]', - ]); - - if (skippedAuthors.has(author)) { - core.info(`Skipping bot PR author ${author}.`); - return; - } - - let isInternalAuthor = false; - try { - const membership = await github.rest.teams.getMembershipForUserInOrg({ - org: projectOwner, - team_slug: internalTeamSlug, - username: author, - }); - isInternalAuthor = membership.data.state === 'active'; - } catch (error) { - if (error.status !== 404) { - throw error; - } - } - - if (isInternalAuthor) { - core.info(`Skipping internal PR author ${author}.`); - return; - } - - const projectResult = await github.graphql( - ` - query($owner: String!, $number: Int!) { - organization(login: $owner) { - projectV2(number: $number) { - id - } - } - } - `, - { owner: projectOwner, number: projectNumber }, - ); - - const projectId = projectResult.organization.projectV2.id; - - await github.graphql( - ` - mutation($projectId: ID!, $contentId: ID!) { - addProjectV2ItemById(input: { - projectId: $projectId, - contentId: $contentId - }) { - item { - id - } - } - } - `, - { - projectId, - contentId: pr.node_id, - }, - ); - - core.info(`Added PR #${pr.number} (${pr.html_url}) to project ${projectOwner}/${projectNumber}.`); diff --git a/.github/workflows/unit-plan.yml b/.github/workflows/unit-plan.yml new file mode 100644 index 00000000000..b427908b26d --- /dev/null +++ b/.github/workflows/unit-plan.yml @@ -0,0 +1,48 @@ +name: Unit plan + +# Why its own workflow: the caller's `needs` gate the whole called workflow, so while this lived +# inside unit-tests.yml it waited on static analysis and typecheck before it could even start — +# and the shards then waited on it. Planning needs neither (its inputs are the checkout, a git +# diff against HEAD^1, the import graph, and the checked-in timing baseline), so hoisting it out +# lets it overlap the gate instead of queueing behind it. Measured: a median 93s off the shard +# matrix's start. +on: + workflow_call: + inputs: + selection_mode: + description: Shadow validates selection; selected applies it only to draft PRs. + required: false + default: shadow + type: string + + outputs: + shards: + description: JSON array of shard assignments for the unit matrix. + value: ${{ jobs.plan.outputs.shards }} + +permissions: + contents: read + +jobs: + plan: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + shards: ${{ steps.plan.outputs.shards }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 2 + persist-credentials: false + - uses: ./.github/actions/install-node-dependencies + - name: Plan unit selection + id: plan + env: + ORCA_UNIT_SELECTION_MODE: ${{ inputs.selection_mode }} + run: node config/scripts/ci-unit-plan.mjs + - uses: actions/upload-artifact@v7 + continue-on-error: true + with: + name: unit-selection-attempt-${{ github.run_attempt }} + path: ci-shards/unit-selection.json + retention-days: 14 diff --git a/.github/workflows/unit-selection-evidence.yml b/.github/workflows/unit-selection-evidence.yml new file mode 100644 index 00000000000..4ff46be84d0 --- /dev/null +++ b/.github/workflows/unit-selection-evidence.yml @@ -0,0 +1,41 @@ +name: Unit selection evidence + +# Why its own workflow: this job is advisory -- `continue-on-error` on both the job and its +# comparison step, so it can never fail a PR. But a caller's `needs: test` waits for every job in +# the called workflow, so while it lived in unit-tests.yml it held `verify` for ~36s after the +# last shard finished. Called as a sibling instead, it still runs on every PR and still uploads +# its review artifact; it just no longer sits on the critical path. +on: + workflow_call: + +permissions: + contents: read + +jobs: + selection_evidence: + if: ${{ !cancelled() }} + continue-on-error: true + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@v6 + with: + sparse-checkout: config/scripts/ci-unit-selection-review.mjs + sparse-checkout-cone-mode: false + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: '24' + - uses: actions/download-artifact@v8 + with: + pattern: unit-shard-node-*-attempt-${{ github.run_attempt }} + path: unit-evidence/ + - name: Compare selection with full results + continue-on-error: true + run: node config/scripts/ci-unit-selection-review.mjs unit-evidence + - uses: actions/upload-artifact@v7 + if: always() + continue-on-error: true + with: + name: unit-selection-review-attempt-${{ github.run_attempt }} + path: unit-evidence/selection-review.json + retention-days: 30 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 9ed96847677..abe9cf28a41 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -14,48 +14,23 @@ on: default: ubuntu-latest type: string - selection_mode: - description: Shadow validates selection; selected applies it only to draft PRs. - required: false - default: shadow + shards: + description: JSON array of shard assignments, produced by unit-plan.yml. + required: true type: string permissions: contents: read jobs: - plan: - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - shards: ${{ steps.plan.outputs.shards }} - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 2 - persist-credentials: false - - uses: ./.github/actions/install-node-dependencies - - name: Plan unit selection - id: plan - env: - ORCA_UNIT_SELECTION_MODE: ${{ inputs.selection_mode }} - run: node config/scripts/ci-unit-plan.mjs - - uses: actions/upload-artifact@v7 - continue-on-error: true - with: - name: unit-selection-attempt-${{ github.run_attempt }} - path: ci-shards/unit-selection.json - retention-days: 14 - test: - needs: plan name: tests node ${{ matrix.node }} ${{ matrix.shard.index }}/${{ matrix.shard.count }} runs-on: ${{ inputs.runner }} strategy: fail-fast: false matrix: node: ${{ fromJSON(inputs.node_versions) }} - shard: ${{ fromJSON(needs.plan.outputs.shards) }} + shard: ${{ fromJSON(inputs.shards) }} steps: - name: Checkout @@ -100,35 +75,6 @@ jobs: retention-days: 14 if-no-files-found: warn - selection_evidence: - needs: test - if: ${{ !cancelled() }} - continue-on-error: true - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@v6 - with: - sparse-checkout: config/scripts/ci-unit-selection-review.mjs - sparse-checkout-cone-mode: false - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: '24' - - uses: actions/download-artifact@v8 - with: - pattern: unit-shard-node-*-attempt-${{ github.run_attempt }} - path: unit-evidence/ - - name: Compare selection with full results - continue-on-error: true - run: node config/scripts/ci-unit-selection-review.mjs unit-evidence - - uses: actions/upload-artifact@v7 - if: always() - continue-on-error: true - with: - name: unit-selection-review-attempt-${{ github.run_attempt }} - path: unit-evidence/selection-review.json - retention-days: 30 - relay_integration: name: relay integration node ${{ matrix.node }} strategy: diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 0a6625f6fe7..19856d6ca84 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -8,6 +8,7 @@ "cloud/**", "resources/licenses/**", ".github/actions/cloud-sql-rollout-lease/**", - ".anti-slop-plugin/**" + ".anti-slop-plugin/**", + "src/main/runtime/__fixtures__/*.timing.json" ] } diff --git a/README.md b/README.md index 0f337f9e677..93e09614609 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca. Autohand Code logo Autohand Code   Charm logo Charm   Cline logo Cline   + CodeBuddy logo CodeBuddy   Codebuff logo Codebuff   Freebuff logo Freebuff   Command Code logo Command Code   diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index e100d54e17a..4e4443354fa 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -8856,7 +8856,7 @@ "Windows ConPTY, SSH-hosted PTYs, app-quit killAll, and daemon dispose retain foreground-tree-only teardown.", "A process born in the capture second is SIGTERMed but not SIGKILLed because ps cannot prove its recycled-PID identity.", "A descendant orphaned before or during root ownership loss requires the separate crash-orphan sweep and is not recovered from a stale kill-time snapshot.", - "recordTerminalSurfaceRetirement advances the INCOMING session's terminal topology revision, so a tombstoned close whose write carries a revision at or below the store's own is rebased away instead of persisting. Pre-existing for every repo already past revision 0 and unchanged by the host-admitted membership fix, which only makes more repos reach that state sooner; the authoritative close path (persistTerminalSurfaceRetirements) computes from the store's session and is unaffected. Known, unaddressed, and deliberately out of scope here: the safe correction is per-tab rather than per-worktree rebase arbitration, because raising the incoming revision wholesale would re-open the host-tab loss whenever a close coincides with a host create.", + "recordTerminalSurfaceRetirement advances the INCOMING session's terminal topology revision, so a tombstoned close whose write carries a revision at or below the store's own is rebased away instead of persisting. Pre-existing for every repo already past revision 0 and unchanged by the host-admitted membership fix, which only makes more repos reach that state sooner; the authoritative close path (stageTerminalSurfaceRetirements) computes from the store's session and is unaffected. Known, unaddressed, and deliberately out of scope here: the safe correction is per-tab rather than per-worktree rebase arbitration, because raising the incoming revision wholesale would re-open the host-tab loss whenever a close coincides with a host create.", "The SSH relay reattach binding (src/main/ssh/ssh-relay-session.ts persistPtyBinding) is NOT flagged host-admitted, so a lease rebind that has to mint a missing tab raises no fence. It rebinds an existing lease rather than admitting a new surface, so it is believed not to need one, but that was not established either way. Known, unaddressed, and deliberately out of scope here.", "The host-created retention journey is macOS-local; the reported incident was on a physical Windows host, which we did not run against." ], diff --git a/config/scripts/bun-profile-change-scope.mjs b/config/scripts/bun-profile-change-scope.mjs index 7b8b47b69aa..dc464a5dc03 100644 --- a/config/scripts/bun-profile-change-scope.mjs +++ b/config/scripts/bun-profile-change-scope.mjs @@ -125,13 +125,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) const changedFiles = readFileSync(process.argv[2], 'utf8').split('\0').filter(Boolean) const result = await classifyBunProfileChanges(changedFiles) console.log(result.reason) - let event = {} - try { - event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')) - } catch { - // Missing event evidence retains full qualification. - } - const policy = bunProfileQualification(changedFiles, result, event) + const policy = bunProfileQualification(changedFiles, result) const output = `should_run=${result.shouldRun}\nqualification=${policy.qualification}\nrunners=${JSON.stringify(policy.runners)}\n` if (process.env.GITHUB_OUTPUT) { appendFileSync(process.env.GITHUB_OUTPUT, output) diff --git a/config/scripts/bun-profile-change-scope.test.mjs b/config/scripts/bun-profile-change-scope.test.mjs index e813be93621..ef21fed5e34 100644 --- a/config/scripts/bun-profile-change-scope.test.mjs +++ b/config/scripts/bun-profile-change-scope.test.mjs @@ -143,6 +143,13 @@ it('keeps all ten platform jobs and runs them when detection is skipped or fails expect(detect.run).toContain('git diff --name-only --no-renames -z HEAD^1 HEAD') expect(workflow.on.pull_request.types).toContain('ready_for_review') expect(workflow.on.schedule).toHaveLength(1) + // A pull request may qualify one platform, so the merged commit must re-qualify all six. + expect(workflow.on.push.branches).toEqual(['main']) + expect(workflow.on.push.paths).toEqual(workflow.on.pull_request.paths) + // The push and schedule paths must not rest on a null property comparison. + expect(workflow.jobs.persistence.if).toContain( + "github.event_name != 'pull_request' || github.event.pull_request.draft != true" + ) expect(workflow.jobs.persistence.strategy.matrix.os).toContain('needs.changes.outputs.runners') for (const jobName of ['linux_glibc_floor', 'linux_musl']) { const job = workflow.jobs[jobName] diff --git a/config/scripts/bun-profile-qualification.mjs b/config/scripts/bun-profile-qualification.mjs index 799f459ff85..dafd137f95e 100644 --- a/config/scripts/bun-profile-qualification.mjs +++ b/config/scripts/bun-profile-qualification.mjs @@ -7,34 +7,35 @@ export const BUN_PERSISTENCE_RUNNERS = [ 'windows-11-arm' ] -const QUALIFICATION_PREFIXES = [ - 'config/', +// Only surfaces whose behaviour actually differs per platform. Escalating on `config/`, +// `resources/` and `.github/` wholesale took 36.5% of the last 1100 commits through all six +// platforms where a platform-flavoured predicate takes 19%. +const PLATFORM_PREFIXES = [ 'native/', - 'resources/', - '.github/', + 'config/patches/', + '.github/actions/install-node-dependencies/', 'src/main/persistence/', 'src/main/sqlite/', 'src/main/orcad/', 'src/main/providers/', 'src/main/daemon/', 'src/main/ssh/', + 'src/main/wsl/', 'src/relay/', 'src/shared/child-process/' ] -export function bunProfileQualification(changedFiles, scope, event = {}) { - const sensitive = changedFiles.some( +export function bunProfileQualification(changedFiles, scope) { + const platformSpecific = changedFiles.some( (file) => + // A root manifest can move a native dependency on every platform at once. !file.includes('/') || - QUALIFICATION_PREFIXES.some((prefix) => file.startsWith(prefix)) || + PLATFORM_PREFIXES.some((prefix) => file.startsWith(prefix)) || /(?:^|[/.-])(?:windows|win32|wsl|macos|darwin|linux|posix|bun)(?:[/.-]|$)/i.test(file) ) - // Only a proven unrelated platform change in a draft may defer qualification. - const full = - event.pull_request?.draft !== true || - changedFiles.length === 0 || - scope.graphUnavailable === true || - sensitive + // A pull request qualifies one platform unless the change is platform-flavoured; the push to + // main re-qualifies all six, so an unescalated miss surfaces minutes after merge, not a day. + const full = changedFiles.length === 0 || scope.graphUnavailable === true || platformSpecific return { qualification: full, runners: full ? BUN_PERSISTENCE_RUNNERS : ['ubuntu-22.04'] diff --git a/config/scripts/bun-profile-qualification.test.mjs b/config/scripts/bun-profile-qualification.test.mjs index 9ebc5e437e1..520cd76fb21 100644 --- a/config/scripts/bun-profile-qualification.test.mjs +++ b/config/scripts/bun-profile-qualification.test.mjs @@ -1,22 +1,32 @@ import { expect, it } from 'vitest' import { BUN_PERSISTENCE_RUNNERS, bunProfileQualification } from './bun-profile-qualification.mjs' -const draft = { pull_request: { draft: true } } const scope = { shouldRun: true } -it('defers only platform qualification for ordinary draft runtime changes', () => { - expect( - bunProfileQualification(['src/main/runtime/rpc/methods/example.ts'], scope, draft) - ).toEqual({ +it('qualifies one platform for a change no platform can alter', () => { + expect(bunProfileQualification(['src/main/runtime/rpc/methods/example.ts'], scope)).toEqual({ qualification: false, runners: ['ubuntu-22.04'] }) }) it.each([ - 'package.json', - 'native/windows-registry/src/addon.cc', + 'src/renderer/src/components/TabStrip.tsx', 'config/vitest.config.ts', + 'config/scripts/ci-unit-plan.mjs', + 'resources/icons/tray.png', + '.github/workflows/pr.yml', + 'docs/reference/agent-status-store.md' +])('keeps one platform for unflavoured input %s', (file) => { + expect(bunProfileQualification([file], scope).runners).toEqual(['ubuntu-22.04']) +}) + +it.each([ + 'package.json', + 'pnpm-lock.yaml', + 'native/windows-registry/src/addon.cc', + 'config/patches/node-pty.patch', + '.github/actions/install-node-dependencies/action.yml', 'src/main/ssh/ssh-provider.ts', 'src/main/providers/local-pty-provider.ts', 'src/shared/child-process/run-process.ts', @@ -28,20 +38,19 @@ it.each([ 'src/main/daemon/entry.ts', 'src/relay/index.ts', 'src/main/wsl/runner.ts' -])('retains all platforms for sensitive input %s', (file) => { - expect(bunProfileQualification([file], scope, draft)).toEqual({ +])('retains all platforms for platform-flavoured input %s', (file) => { + expect(bunProfileQualification([file], scope)).toEqual({ qualification: true, runners: BUN_PERSISTENCE_RUNNERS }) }) -it('qualifies every ready commit, scheduled/manual runs and incomplete evidence', () => { - const paths = ['src/main/runtime/rpc/methods/example.ts'] - for (const event of [{}, { pull_request: { draft: false } }]) { - expect(bunProfileQualification(paths, scope, event).qualification).toBe(true) - } - expect(bunProfileQualification([], scope, draft).qualification).toBe(true) +it('fails closed to every platform when the evidence is incomplete', () => { + expect(bunProfileQualification([], scope).qualification).toBe(true) expect( - bunProfileQualification(paths, { ...scope, graphUnavailable: true }, draft).qualification + bunProfileQualification(['src/main/runtime/rpc/methods/example.ts'], { + ...scope, + graphUnavailable: true + }).qualification ).toBe(true) }) diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 2ca7094bdd2..664e8268662 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -1721,7 +1721,6 @@ "src/main/git/remove-worktree-branch-cleanup.test.ts": 163, "src/main/git/remove-worktree-clean-preflight.test.ts": 125, "src/main/git/remove-worktree.test.ts": 159, - "src/main/git/repo-api-parity.test.ts": 194, "src/main/git/repo-branch-conflict-batched-probe.test.ts": 73, "src/main/git/repo-branch-conflict-real-git.test.ts": 351, "src/main/git/repo-branch-conflict.test.ts": 214, @@ -3241,7 +3240,6 @@ "src/main/runtime/agent-session-launch-env-backfill.test.ts": 264, "src/main/runtime/agent-session-lease-renewal.test.ts": 242, "src/main/runtime/agent-session-operation-admission.test.ts": 165, - "src/main/runtime/agent-session-orphan-child-reaper.test.ts": 90, "src/main/runtime/agent-session-process-identity-probe-windows-batch.test.ts": 47, "src/main/runtime/agent-session-process-identity-probe.test.ts": 78, "src/main/runtime/agent-session-provider-handle-transition.test.ts": 104, diff --git a/config/scripts/ci-unit-plan.mjs b/config/scripts/ci-unit-plan.mjs index 28eed60f8e8..db7a13070d0 100644 --- a/config/scripts/ci-unit-plan.mjs +++ b/config/scripts/ci-unit-plan.mjs @@ -3,7 +3,7 @@ import { pathToFileURL } from 'node:url' import { runProcessSync } from './script-child-process.mjs' import { collectUnitDependencyGraph } from './ci-unit-dependency-graph.mjs' import { discoverUnitFiles } from './ci-unit-files.mjs' -import { planUnitSelection } from './ci-unit-selection.mjs' +import { FULL_SHARD_COUNT, planUnitSelection } from './ci-unit-selection.mjs' import { readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs' export function prepareUnitPlan(env = process.env) { @@ -39,7 +39,10 @@ export function prepareUnitPlan(env = process.env) { files, candidateFiles: files, executionFiles: files, - shards: Array.from({ length: 8 }, (_, index) => ({ index: index + 1, count: 8 })) + shards: Array.from({ length: FULL_SHARD_COUNT }, (_, index) => ({ + index: index + 1, + count: FULL_SHARD_COUNT + })) } } writeAssignment('ci-shards/unit-selection.json', plan) diff --git a/config/scripts/ci-unit-plan.test.mjs b/config/scripts/ci-unit-plan.test.mjs index 1711582b64f..eda7903ae1d 100644 --- a/config/scripts/ci-unit-plan.test.mjs +++ b/config/scripts/ci-unit-plan.test.mjs @@ -4,6 +4,7 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { expect, it } from 'vitest' import { runProcessSync } from './script-child-process.mjs' +import { FULL_SHARD_COUNT } from './ci-unit-selection.mjs' it.each([true, false])( 'plans from a real Git diff, with parent evidence available: %s', @@ -70,7 +71,7 @@ it.each([true, false])( ? 'Transitive imports plus indirect-input consumers' : 'Error: Changed paths unavailable' ) - expect(plan.shards).toHaveLength(withParent ? 1 : 8) + expect(plan.shards).toHaveLength(withParent ? 1 : FULL_SHARD_COUNT) } finally { rmSync(root, { recursive: true, force: true }) } diff --git a/config/scripts/ci-unit-selection.mjs b/config/scripts/ci-unit-selection.mjs index 70894dde0a1..841bad422e7 100644 --- a/config/scripts/ci-unit-selection.mjs +++ b/config/scripts/ci-unit-selection.mjs @@ -27,14 +27,20 @@ export function selectUnitFiles(files, changed, graph) { } } +// A concurrency slot is charged per job, not per core, so eight 6.5-minute shards cost eight of +// the account's slots and made the unit matrix 68% of daily slot demand. Against the checked-in +// baseline, five shards each carry 24.7 test-minutes over four workers plus ~2.6 minutes of fixed +// setup, so ~8.8 minutes -- less than the 10.5-minute p95 queue the oversharding was causing. +export const FULL_SHARD_COUNT = 5 + export function planUnitSelection({ files, changed, graph, timings, event, mode = 'shadow' }) { const candidate = selectUnitFiles(files, changed, graph) const selected = mode === 'selected' && event?.pull_request?.draft === true && !candidate.full const executionFiles = selected ? candidate.files : files const totalMs = balanceFiles(executionFiles, 1, timings).shards[0].durationMs const count = selected - ? Math.max(1, Math.min(8, Math.ceil(totalMs / 900_000), executionFiles.length)) - : 8 + ? Math.max(1, Math.min(FULL_SHARD_COUNT, Math.ceil(totalMs / 900_000), executionFiles.length)) + : FULL_SHARD_COUNT return { version: 1, mode: selected ? 'selected' : 'shadow', diff --git a/config/scripts/ci-unit-selection.test.mjs b/config/scripts/ci-unit-selection.test.mjs index 37338cbfc6d..69370a675f6 100644 --- a/config/scripts/ci-unit-selection.test.mjs +++ b/config/scripts/ci-unit-selection.test.mjs @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest' import { buildUnitDependencyGraph } from './ci-unit-dependency-graph.mjs' -import { auditUnitSelection, planUnitSelection, selectUnitFiles } from './ci-unit-selection.mjs' +import { + FULL_SHARD_COUNT, + auditUnitSelection, + planUnitSelection, + selectUnitFiles +} from './ci-unit-selection.mjs' const sources = new Map( Object.entries({ @@ -79,6 +84,17 @@ describe('conservative unit selection', () => { expect(selected.shards).toEqual([{ index: 1, count: 1 }]) }) + it('spends five concurrency slots on a full run', () => { + const plan = planUnitSelection({ files, changed: ['src/leaf.ts'], graph, timings: {} }) + expect(plan.shards).toEqual( + Array.from({ length: FULL_SHARD_COUNT }, (_, index) => ({ + index: index + 1, + count: FULL_SHARD_COUNT + })) + ) + expect(FULL_SHARD_COUNT).toBe(5) + }) + it('records failures that would have been missed while shadow runs remain full', () => { const plan = planUnitSelection({ files, changed: ['src/leaf.ts'], graph, timings: {} }) expect( diff --git a/config/scripts/mobile-recording-pin-checkout.test.mjs b/config/scripts/mobile-recording-pin-checkout.test.mjs index f45db12bdbe..7ff6251afd6 100644 --- a/config/scripts/mobile-recording-pin-checkout.test.mjs +++ b/config/scripts/mobile-recording-pin-checkout.test.mjs @@ -19,12 +19,15 @@ it('keeps full ancestry and credentials for lazy pinned-tree reads', () => { expect(job.if).toBeUndefined() expect(workflow.on.push.branches).toEqual(['main']) expect(workflow.concurrency.group).toContain('github.sha') - expect(job.steps.find((step) => step.name === 'Check the recording pin is reachable').run).toBe( - 'pnpm exec tsx scripts/rpc-recording-pin-guard.mts ancestry' - ) + const reachable = job.steps.find((step) => step.name === 'Check the recording pin is reachable') + expect(reachable.run).toBe('pnpm exec tsx scripts/rpc-recording-pin-guard.mts reachable') const reproduce = job.steps.find( (step) => step.name === 'Reproduce the corpus from the pinned tree' ) + // Both steps ask GitHub which pull requests hold a pin main's history lacks. + expect(job.permissions).toEqual({ contents: 'read', 'pull-requests': 'read' }) + expect(reachable.env.GITHUB_TOKEN).toContain('github.token') + expect(reproduce.env.GITHUB_TOKEN).toContain('github.token') expect(reproduce.run).toContain('reproduce --if-changed-since "$PIN_GUARD_BASE"') expect(reproduce.run).toContain( 'else\n pnpm exec tsx scripts/rpc-recording-pin-guard.mts reproduce\nfi' diff --git a/config/scripts/pr-code-change-scope.test.mjs b/config/scripts/pr-code-change-scope.test.mjs index c23ea0e1140..1981623d281 100644 --- a/config/scripts/pr-code-change-scope.test.mjs +++ b/config/scripts/pr-code-change-scope.test.mjs @@ -597,10 +597,17 @@ describe('PR Checks skip wiring', () => { } expect(prWorkflow.jobs.test.needs).toEqual([ 'code_paths', + 'unit_plan', 'test_native_cache', 'static_analysis', 'typecheck' ]) + // Planning is deliberately NOT behind the static-analysis gate: it consumes nothing those + // jobs produce, so gating it only made the shards queue behind it. It still has to succeed + // before the shards run, or the matrix would expand from an empty assignment. + expect(prWorkflow.jobs.unit_plan.needs).toEqual(['code_paths']) + expect(prWorkflow.jobs.unit_plan.if).toBe("needs.code_paths.outputs.test == 'true'") + expect(prWorkflow.jobs.test.if).toContain("needs.unit_plan.result == 'success'") expect(prWorkflow.jobs.test.if).toContain("needs.code_paths.outputs.test == 'true'") expect(prWorkflow.jobs.test.if).toContain("needs.test_native_cache.result == 'success'") expect(prWorkflow.jobs.test.if).toContain("needs.test_native_cache.result == 'skipped'") diff --git a/config/scripts/pr-workflow-parallelism.test.mjs b/config/scripts/pr-workflow-parallelism.test.mjs index 6804527fe3c..dde93505193 100644 --- a/config/scripts/pr-workflow-parallelism.test.mjs +++ b/config/scripts/pr-workflow-parallelism.test.mjs @@ -7,7 +7,6 @@ import { MOBILE_WEB_APP_DEPENDENCIES_REQUIRED_ENV } from './mobile-web-app-bundl const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) const prTestLocWorkflow = parse(readFileSync('.github/workflows/pr-test-loc.yml', 'utf8')) -const trackingWorkflow = parse(readFileSync('.github/workflows/track-community-prs.yaml', 'utf8')) const releasePolicyWorkflow = parse(readFileSync('.github/workflows/release-policy.yml', 'utf8')) const issueLabelWorkflow = parse(readFileSync('.github/workflows/issue-os-labeler.yaml', 'utf8')) const unitTestWorkflow = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8')) @@ -55,7 +54,6 @@ describe('PR workflow parallelism', () => { expect(workflow.jobs.typecheck['runs-on']).toBe('ubuntu-24.04-arm') expect(workflow.jobs.verify['runs-on']).toBe('ubuntu-slim') expect(prTestLocWorkflow.jobs.loc['runs-on']).toBe('ubuntu-slim') - expect(trackingWorkflow.jobs['track-community-pr']['runs-on']).toBe('ubuntu-slim') expect(releasePolicyWorkflow.jobs.enforce['runs-on']).toBe('ubuntu-slim') expect(issueLabelWorkflow.jobs['apply-os-label']['runs-on']).toBe('ubuntu-slim') }) @@ -107,8 +105,12 @@ describe('PR workflow parallelism', () => { expect(nodeNextWorkflow.on.schedule).toHaveLength(1) expect(nodeNextWorkflow.on.workflow_dispatch).toBeNull() expect(sharedTest.strategy.matrix.node).toBe('${{ fromJSON(inputs.node_versions) }}') - expect(sharedTest.strategy.matrix.shard).toBe('${{ fromJSON(needs.plan.outputs.shards) }}') - expect(sharedTest.needs).toBe('plan') + expect(sharedTest.strategy.matrix.shard).toBe('${{ fromJSON(inputs.shards) }}') + // The shard matrix no longer needs an in-workflow plan job: planning moved to + // unit-plan.yml so it can run before the static-analysis gate clears. + expect(sharedTest.needs).toBeUndefined() + expect(unitTestWorkflow.jobs.plan).toBeUndefined() + expect(unitTestWorkflow.on.workflow_call.inputs.shards.required).toBe(true) expect(installStep.with['node-version']).toBe('${{ matrix.node }}') expect(installStep.with['cache-electron-package']).toBe('true') expect(testStep.run).toContain('--shard=${{ matrix.shard.index }}/${{ matrix.shard.count }}') @@ -120,7 +122,7 @@ describe('PR workflow parallelism', () => { expect(workflow.jobs.test.needs).toContain('test_native_cache') expect(nodeNextPrimerInstall.with['native-runtime']).toBe('node') expect(nodeNextPrimerInstall.with['node-version']).toBe('${{ matrix.node }}') - expect(nodeNextWorkflow.jobs.test.needs).toEqual(['test_native_cache']) + expect(nodeNextWorkflow.jobs.test.needs).toEqual(['test_native_cache', 'unit_plan']) }) it('runs real-shell coverage once outside the general shards', () => { @@ -497,6 +499,22 @@ describe('PR workflow parallelism', () => { } }) + it('keeps advisory unit-selection evidence off the gate', () => { + // It is continue-on-error, so it can never fail a PR. Living inside unit-tests.yml made a + // caller's `needs: test` wait for it anyway, holding verify ~36s past the last shard. Pinned + // here so it cannot drift back onto the critical path. + const evidence = workflow.jobs.unit_selection_evidence + expect(evidence.uses).toBe('./.github/workflows/unit-selection-evidence.yml') + expect(evidence.needs).toEqual(['test']) + expect(workflow.jobs.verify.needs).not.toContain('unit_selection_evidence') + expect(unitTestWorkflow.jobs.selection_evidence).toBeUndefined() + const evidenceWorkflow = parse( + readFileSync('.github/workflows/unit-selection-evidence.yml', 'utf8') + ) + const job = evidenceWorkflow.jobs.selection_evidence + expect(job['continue-on-error']).toBe(true) + }) + it('keeps verify as the aggregate required check', () => { expect(workflow.jobs.verify.needs).toEqual([ 'code_paths', diff --git a/config/tsconfig.cli.json b/config/tsconfig.cli.json index 030a90632b0..85f3529b2dc 100644 --- a/config/tsconfig.cli.json +++ b/config/tsconfig.cli.json @@ -9,6 +9,7 @@ "../src/main/claude/claude-hook-event-versions.ts", "../src/main/claude/claude-managed-hook-events.ts", "../src/main/qoder/hook-service.ts", + "../src/main/codebuddy/hook-service.ts", "../src/main/agent-hooks/hook-stdin-contract.ts", "../src/main/agent-hooks/hook-post-command.ts", "../src/main/agent-hooks/hook-config-write-path.ts", diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index 9fbda727d28..af45e38a72c 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 82m + + downloads: 83m @@ -15,7 +15,7 @@ downloads downloads - 82m - 82m + 83m + 83m diff --git a/docs/reference/ci-demand-rollout.md b/docs/reference/ci-demand-rollout.md index 8219a3401cb..081d276c4bf 100644 --- a/docs/reference/ci-demand-rollout.md +++ b/docs/reference/ci-demand-rollout.md @@ -45,7 +45,10 @@ suite. A shard verifies the plan's source SHA and complete discovery list before using it. Missing/stale artifacts fall back to full coverage, even if that means running the suite on fewer shards. -The initial policy is **shadow**, with all eight shards retained. The first +The initial policy is **shadow**, with every shard retained. A full run spends +`FULL_SHARD_COUNT` (five) concurrency slots: the account is charged a slot per job +rather than per core, so eight 6.5-minute shards made this matrix 68% of daily slot +demand while the arm pool queued 10.5 minutes at p95. The first local inventory matched Vitest exactly (9,950 files at validation); representative source changes retained roughly 88% of files because of indirect input readers. That is evidence for conservative coverage, not evidence of the analysis's diff --git a/docs/reference/codebuddy-harness.md b/docs/reference/codebuddy-harness.md new file mode 100644 index 00000000000..e59eaec855e --- /dev/null +++ b/docs/reference/codebuddy-harness.md @@ -0,0 +1,44 @@ +# CodeBuddy terminal harness + +CodeBuddy uses its own identity in the desktop and mobile agent catalogs, process +detection, telemetry, hooks, resume records and AI Vault. `codebuddy` and `cbc` +identify the interactive CLI; print, server, ACP and detached background modes do +not identify an interactive pane. Model and effort selections use `--model` and +`--effort`; the CLI resolves its stable model aliases for the current account. + +Managed hooks use the existing Claude-compatible installer and transport, with +CodeBuddy's own `.codebuddy/settings.json` and hook source. Installation preserves +user hooks and statusline configuration. Local and SSH installers share the same +plan; Windows explicitly selects CodeBuddy's supported PowerShell hook shell. +Status flows through the execution host's canonical hook store. + +## Observed lifecycle + +Verified with the authenticated CodeBuddy 2.159.0 CLI on macOS: + +- `UserPromptSubmit` starts work. `SessionStart` can arrive afterward and must not + settle that work. +- An unanswered `AskUserQuestion` emits a `Notification` with + `notification_type: permission_prompt` and the message + `needs your permission to use AskUserQuestion`. +- In this version, `PreToolUse(AskUserQuestion)` arrives **after** the user answers; + it resumes working, followed by `PostToolUse` and `Stop`. +- `Stop` supplies the final assistant message and the provider session identity + supplies the resume command. + +The sanitized real event sequence is +`src/shared/__fixtures__/codebuddy-question-hooks.jsonl`. The regression test +replays it through the shared hook listener. Qoder keeps its existing lifecycle +semantics while sharing the common event projection. + +AI Vault reads CodeBuddy's `type: message` JSONL records with top-level roles and +`input_text` / `output_text` content blocks. Local, WSL and SSH discovery use the +provider's `.codebuddy/projects` tree and the same incremental parser. + +## Validation scope + +Live macOS checks exercised launch through Orca's agent menu, model switching, +question waiting, answer submission, working and completion indicators, history +discovery and a resumed session recalling its earlier answer. Hidden-renderer CDP +screenshots record the working, question and completed states. Windows, Linux, +WSL and SSH runtime execution have not been exercised live on this machine. diff --git a/docs/site/content/docs/agents/supported.mdx b/docs/site/content/docs/agents/supported.mdx index 0658ce6fd8c..ecc8cfd1305 100644 --- a/docs/site/content/docs/agents/supported.mdx +++ b/docs/site/content/docs/agents/supported.mdx @@ -45,6 +45,7 @@ To restore prompts for one agent only, edit that agent's default arguments or en | Auggie | Auto-setup | [Augment](https://docs.augmentcode.com/cli/overview) | | Autohand | Auto-setup | [Autohand](https://github.com/autohandai/code-cli) | | Cline | Auto-setup | [Cline](https://docs.cline.bot/cline-cli/overview) | +| CodeBuddy | `npm install -g @tencent-ai/codebuddy-code` | [CodeBuddy](https://www.codebuddy.ai/cli) | | Codebuff | Auto-setup | [Codebuff](https://www.codebuff.com/docs/help/quick-start) | | Freebuff | Auto-setup | [Freebuff](https://freebuff.com/cli) | | Command Code | Auto-setup, status | [Command Code](https://commandcode.ai/docs/quickstart) | diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index c65008db7ce..711ee41ffd3 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -24,6 +24,7 @@ import { ensureDesktopNotificationChannel } from '../src/notifications/desktop-n import { loadHostCatalog } from '../src/transport/host-store' import { extractPairingCodeFromUrl } from '../src/transport/pairing' import { recoverMobileRelayPairing } from '../src/transport/mobile-relay-pairing-recovery' +import { appUpdateChecker } from '../src/app-update/app-update-runtime' // Why: keeps the native splash screen visible until the React tree is mounted // and ready to render. Without this the user sees a blank white/black frame @@ -65,6 +66,8 @@ export default function RootLayout() { // Why: a rolled APNs/FCM token stops delivering silently, so every paired host // has to be re-registered with the new one as soon as the provider hands it over. useEffect(() => startPushTokenSync(), []) + // Cold start, then foreground and timer checks on the desktop updater's cadence. + useEffect(() => appUpdateChecker.start(), []) useEffect(() => startAndroidForegroundPushPresentation(), []) // Why: route `orca://pair?...` deep links to the confirm screen so diff --git a/mobile/app/settings.tsx b/mobile/app/settings.tsx index eb954a8e83d..8b3267e2f86 100644 --- a/mobile/app/settings.tsx +++ b/mobile/app/settings.tsx @@ -2,6 +2,7 @@ import { Linking } from 'react-native' import { useRouter } from 'expo-router' import SettingsMenuScreen from '../src/settings/settings-menu-screen' import { PendingCredentialCleanupCard } from '../src/settings/pending-credential-cleanup-card' +import { SettingsAppUpdateSection } from '../src/settings/settings-app-update-section' export default function NativeSettingsRoute() { const router = useRouter() @@ -10,6 +11,7 @@ export default function NativeSettingsRoute() { push={(route) => router.push(route)} openExternal={(url) => Linking.openURL(url)} > + ) diff --git a/mobile/rpc-foundation/goldens/aivault-history-scan-fulfilled.json b/mobile/rpc-foundation/goldens/aivault-history-scan-fulfilled.json index 419193c2634..05d48255545 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb", diff --git a/mobile/rpc-foundation/goldens/aivault-history-scan-unsupported.json b/mobile/rpc-foundation/goldens/aivault-history-scan-unsupported.json index 88e8e31ab90..c49eaddae91 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb", 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 ebcfd83d05e..b83d7aa6db8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb", diff --git a/mobile/rpc-foundation/goldens/aivault-history-screen-listed.json b/mobile/rpc-foundation/goldens/aivault-history-screen-listed.json index 9b06260c75c..05c1f3a3d2b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 123c47efce2..5ede6eb120b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7273d709642..02164967d24 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 588b00e9d27..54b025004f9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 66ebfa06180..55d31ba3d5d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8906ed097a1..48db573bca7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fa0884b1835..091cbba9667 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6b10cde0937..722569f79e4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 241b9e64da0..ca728868ddd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8697a194d78..cbad473015a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb", diff --git a/mobile/rpc-foundation/goldens/b1.json b/mobile/rpc-foundation/goldens/b1.json index e9857f7b6c8..fb50a2faf88 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/b2.json b/mobile/rpc-foundation/goldens/b2.json index 8412643f6a6..df8f39f589b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/b3.json b/mobile/rpc-foundation/goldens/b3.json index 922c2db368e..19d44634a48 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/browser-dialog-accepted.json b/mobile/rpc-foundation/goldens/browser-dialog-accepted.json index 5013ef328de..cabb31e6a66 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/browser-dialog-dismissed.json b/mobile/rpc-foundation/goldens/browser-dialog-dismissed.json index 4e23c9bbfb8..da8036a9aea 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/browser-keyboard-input.json b/mobile/rpc-foundation/goldens/browser-keyboard-input.json index 0e5aab769a1..11721f74506 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e0918d56639..bd6c2112a32 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2a079f64ab8..83476c28672 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "55971752f963202d30160851197a301089b0f3ebd0c46725af1a461d8310d658", diff --git a/mobile/rpc-foundation/goldens/browser-wheel-scrolled.json b/mobile/rpc-foundation/goldens/browser-wheel-scrolled.json index bcbc82088a6..94ad174a33e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e3c40477b44..1f5f6ec7abe 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e4a42c97e52..577c84ef759 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6ddb969723e..bb828ba1673 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 acaf59fdb3c..b98b684f5eb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5e6495076e0..2f800d92b7a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7102d030df0..1a1d3056c2d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5ec35b41464..6f0b9baf187 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 11b545e8c87..dc1bad47e5c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c5c41bb2d99..5f05ac6746a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ae20906e3b9..f117d764a10 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c397c9741ac..ba7be42c230 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "76b53dea504a688493843e23e8e7d052fc196f559bb1e2579d0fd38bf3156d0e", diff --git a/mobile/rpc-foundation/goldens/components-codex-capability.json b/mobile/rpc-foundation/goldens/components-codex-capability.json index bc682d8f7c6..298a0a94eb5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/components-setup-ask.json b/mobile/rpc-foundation/goldens/components-setup-ask.json index 626c1640ff1..583273d6181 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/components-target-local.json b/mobile/rpc-foundation/goldens/components-target-local.json index de63b743df2..cb200b89d4d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", diff --git a/mobile/rpc-foundation/goldens/components-target-ssh.json b/mobile/rpc-foundation/goldens/components-target-ssh.json index 6862b608e8c..b76ff775677 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2431b6b6bb5..82b54bda98e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3881b54995e..fe7dd7a58b5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0d66e1a803c..5bf8483bd59 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 170207631f8..da678dc98e2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/diff-review-snapshot.json b/mobile/rpc-foundation/goldens/diff-review-snapshot.json index 56c1562d77a..ad6442801d5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9029cc36b31..7eca0de25a8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c4db11ee782..5202ad86447 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ddbb1a4028a..78516f75ee7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9f0c24dc1ca..a50d9ea8e1a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 25480522f09..564f53ff194 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8a5ca5a0160..012d31f7fc7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ab76b124fb4..8213f3e4961 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5c10140877d..b8c88b89a28 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "7a42a348f4407b94cf75ac77a4b6b783b7d28103b1ae1d111d2708dab0dd4a0c", diff --git a/mobile/rpc-foundation/goldens/files-explorer-readdir.json b/mobile/rpc-foundation/goldens/files-explorer-readdir.json index 658cd1cba1c..fb6bd93a03d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "7a42a348f4407b94cf75ac77a4b6b783b7d28103b1ae1d111d2708dab0dd4a0c", diff --git a/mobile/rpc-foundation/goldens/files-ownership-local.json b/mobile/rpc-foundation/goldens/files-ownership-local.json index f3815b3c7f5..6da2e1bc832 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-ownership-ssh.json b/mobile/rpc-foundation/goldens/files-ownership-ssh.json index 95aac7fc246..94bd352a764 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4694bdef9d5..ab8ac02d2a6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 index dcf696662f1..4a232dcf21e 100644 --- a/mobile/rpc-foundation/goldens/files-preview-artifact-image-read.json +++ b/mobile/rpc-foundation/goldens/files-preview-artifact-image-read.json @@ -3,7 +3,7 @@ "family": "files.preview-artifact-image", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-preview-artifact-image.json b/mobile/rpc-foundation/goldens/files-preview-artifact-image.json index f9934fbb841..50f04a169e7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c1ea1ae07af..501314f8808 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 index ab57d829624..797e0f5c7bd 100644 --- a/mobile/rpc-foundation/goldens/files-preview-worktree-image-read.json +++ b/mobile/rpc-foundation/goldens/files-preview-worktree-image-read.json @@ -3,7 +3,7 @@ "family": "files.preview-worktree-image", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-preview-worktree-image.json b/mobile/rpc-foundation/goldens/files-preview-worktree-image.json index fc900664173..3202b46f6d5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 index 9f189b98b7c..b67eb6d59cc 100644 --- a/mobile/rpc-foundation/goldens/files-preview-worktree-text-read.json +++ b/mobile/rpc-foundation/goldens/files-preview-worktree-text-read.json @@ -3,7 +3,7 @@ "family": "files.preview-worktree-text", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-preview-worktree.json b/mobile/rpc-foundation/goldens/files-preview-worktree.json index 5fd91b2b517..933942cfdb3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-save-blind.json b/mobile/rpc-foundation/goldens/files-save-blind.json index dc1209252f7..7dc10c03d1b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/files-save-verified.json b/mobile/rpc-foundation/goldens/files-save-verified.json index 3d370939df4..12bb53d22b7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4ba829fd513..0658bd72712 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", diff --git a/mobile/rpc-foundation/goldens/home-host-accounts.json b/mobile/rpc-foundation/goldens/home-host-accounts.json index b9c58502f53..e702147addf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c632fdbc4b730777ecb09f08bec14cca0586042b01ed99d40d0228806c7def4a", diff --git a/mobile/rpc-foundation/goldens/home-host-stats.json b/mobile/rpc-foundation/goldens/home-host-stats.json index 53128ac55c8..8cf4e0187da 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ff27183bf08..adadbb81803 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 49517faa096..f1de161f46a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4d4f1c5da7b..ca8bb14a5df 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0e711a5623e..b8ac4ff58b0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "8e41c8624b9b6185e447cee3590a851ab88b6b1ee1d632af90e3a771a90310be", diff --git a/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json b/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json index 38f5f2d52db..dabe2fc5872 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c113f4934a8..100f21460dd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/inventory-lifecycle.json b/mobile/rpc-foundation/goldens/inventory-lifecycle.json index c8e58c17a54..63331d4bc2c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/inventory-repeat-query.json b/mobile/rpc-foundation/goldens/inventory-repeat-query.json index cca240fab8a..133dd37378e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", diff --git a/mobile/rpc-foundation/goldens/lifecycle-b3.json b/mobile/rpc-foundation/goldens/lifecycle-b3.json index cf04a8e52cd..7e2a3ff2dde 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", diff --git a/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json b/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json index 7706e5c23b4..28cd115186d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c02ba959c78..7ee44f37fc4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 233de8a6418..9c5f08c680e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6c7c5a921cd..c72a6b5d824 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/linear-select-workspace.json b/mobile/rpc-foundation/goldens/linear-select-workspace.json index c62341321b8..cfe5c67870f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8aeb392fe07..db71c42b710 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4d2fe5a4e75..8d205778875 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c42f108854c..791ede81d7d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 522a7066907..fe99a2b2814 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 53404a56db0..68b76baf798 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb", 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 a2daea98baa..3d60a85ab04 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ace3ee82e9e..2fc02ff1861 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3", 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 6255151ee8e..4f9bbfc16b7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2dc1ed6dd78..5530b93969e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb", 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 512d5c031a1..175556b1b0c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 de17109c622..241e0f99409 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fa432bebe88..74f84d2526f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0bbdd00cb21..6da9cceba8b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cd482560c1a..d943a202d3f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fb7f153f204..1311ac78659 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 29a9173986d..2b425803af0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1e6456b1f02..f4219c2aeb6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d6009d614a7..1e2c03760f2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 facb0d19e0e..43bed4caabf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 87ac6b7faaf..ca312d82abb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2c52ef6ec72..8ee48dd1fd3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2e7e89decf2..99d3eb12699 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 300c0a1bb4f..ddda539bc4c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8599f5b2df9..f464ff8ef8e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 70fa3aaa084..9879f9282a3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1829baa8eda..ea1cb61598f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a56cfbad645..cb7bae2e7aa 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4bc97abed12..5b35c3911fb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 53d45631f5f..288f5e269b5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 68684251c5e..86add409131 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 19650d12990..d1d9e1bd99e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 18a8f25f594..965623d6156 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e3ae350bcd8..cc88c5ad7b9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "7a42a348f4407b94cf75ac77a4b6b783b7d28103b1ae1d111d2708dab0dd4a0c", 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 131d7707a4e..8e1d86de1b9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "7a42a348f4407b94cf75ac77a4b6b783b7d28103b1ae1d111d2708dab0dd4a0c", 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 9c6bccc9609..2b192f3aa84 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 3b5bec1f7d5..7c1c3ac751c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bbf0b385173..5cb190a31ef 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 index 732fbc1f4b9..a3a8fe88951 100644 --- 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 @@ -3,7 +3,7 @@ "family": "files.preview-artifact-image", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 0d0a849b2f8..159404e1b4c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 8760719727d..e7b369f7a8f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 a25e815bce3..377bcb8ef86 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 51b9cf78628..32817b0fb41 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 363e3c7dde8..78d0ee50808 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 index 0408a9c3a58..8173a17b650 100644 --- 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 @@ -3,7 +3,7 @@ "family": "files.preview-worktree-image", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 index 27825102bce..34e77850885 100644 --- 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 @@ -3,7 +3,7 @@ "family": "files.preview-worktree-text", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 35d97c64c46..8312731b553 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 8a458180dd2..738bb631480 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 c25f92c59f7..a4c5d13082c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", 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 80494c6f26f..e340b90e735 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1eea79733e9..e28a23102bb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 aabcd9eac47..ec13c65fb4a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 02f2730a3ce..25f00c15ba2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 81be16d5e78..cbdea59fbc9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4e9c254248b..8aec0ab46c9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 acf44fb0606..bcf54ece9dd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 be1ce32089c..5afd179d85d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2d358a3e83b..b84b036551c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9236f81f5d5..a3578fa6780 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0ad229114d0..ce4246cb49b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 be9e8c3ccbb..32b2fd268db 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 43b1e6f6970..675c778ab31 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4f3bd614da3..092dd3ae535 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b1c08de7bfc..c5b8fa1ccec 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4bc338b638c..d69605a772c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 806b0ebafb4..aa34ebfeea0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 77d912eb07c..e9d6280e362 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 76bc1ef55d1..cecc525a4ca 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 8af2ce1ce22..294c5a2695b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 30555b5ae0c..ce8130e211c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 396b4c7439e..bc31d8f4854 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 ab3f280a28a..2030a3897ea 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 28b45178e0d..3fa4c425789 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 973e5712cfc..9bdbddba0a7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 336ae110059..f11f6dd43a5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 a65428b8e2c..52377ce2bb4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 3ba174a751d..e6031d2899b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 bf05e864622..8a746b7d508 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 a4102369042..aedb53ac141 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 d51a8e4a893..60936c24937 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 29461297379..890ffd08100 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 03b1f3af3d6..7c620ace6af 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 9dfcea1975d..84e3d8d7ec4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 db37ecace10..7b869bba869 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 8c3f240d5fa..5e3e117809b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e808bddbe6f..84817f85a31 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8188118fef3..8e35b0ac7e3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0ffb1635027..7cbe3edb79d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a7d857302e1..94c83f0f737 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 122408a75f7..45f37cca989 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 57adfc6c194..6d3c7f6f94f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2cf5ee33fba..0e7def82628 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0cf874ee84e..404a2f89f26 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bf8d61caa05..04f692cca20 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ec3546ea274..5098386c4f5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fb33a1c1e7d..e1b7fa432b8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b7b4f4e326e..2722a3dc179 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 39ea426a9f6..e57ec82457a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 445b6cee00a..4a2305bcd49 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 425cb014bb3..f946583aa4f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a7ec45c47ea..7463a1bc89b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ca99bab6a01..627f7488bde 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 11263c70c66..831261fe71b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 13f402cfa97..c9180917cc6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 36e2ddbc10f..b7a96e01c98 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4f6eb1557dc..3e26f5eb9e9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f3e587029bd..e701009162c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2bc0f6d3f4f..85f85213377 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2380040f668..edbb82cccad 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2beeb0ed5ab..15bc63967ae 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c0176e34319..190bdc01828 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3c160667be9..b0453b1c0fc 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 033302a64a2..bc4f8e5dac8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 61665af7c47..d71ede67479 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a75e8c682c5..4c50612fc18 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 60cc966dac4..e6ff652f014 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d113789d15e..1dcbcdbfe1b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4f9be474280..7088f794abf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 29589903a21..9da3e6beaf7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8f6874667e4..04d990d591c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 79ffed55a50..39f2a2e6386 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 454bbe31091..1bc86e91896 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6ec8daed6ed..151808d673b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bba304fd510..5126ef54e29 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 index b917722c798..7573ef7cec0 100644 --- a/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-app-js.json +++ b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-app-js.json @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-fetch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", 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 index 79b074049b6..43a231817bd 100644 --- a/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-head.json +++ b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-head.json @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-fetch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", 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 index 338446476fe..29fba15b4da 100644 --- a/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-tail.json +++ b/mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-tail.json @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-fetch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", 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 index 19d7ac1f881..fe3ed6f7f9d 100644 --- 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 @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-fetch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", 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 index 8f7a243fa73..4abd8f4e375 100644 --- 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 @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-manifest", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", 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 8952e42cef8..70d9caacc86 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 81df706c7ca..bda0f2377a6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fbf47253e12..6ca1f29ca74 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1f713a67286..de35c7e8e9d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 09928edbaca..4828a3c136a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2ad6a4c3168..e27b8dedce8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 08806b12b12..5c96345be12 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2a1c1f9bf05..6906e28b4be 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d70c83c9f1b..2d604afd9c4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8ac93debb4b..2a43ab0c76e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0b3f26918a6..afae0c3d35a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "f55275c268e49679b37d72ce6cbe71186ce02c77fe3fd53f0f21e796502d62e8", 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 881d94162ed..e06cd59edc4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 58b63cc9169..0792eb91391 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 07b3c9a2796..dcf41dad6a0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ebe7a717cc0..51f9cab8a2d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 7810e8b1569..eb571950293 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 44781877b88..99d22ea0bd3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 e18957b2625..ea72c6502d2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 69986253a7d..668b9c84bf9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bae589eabc7..7c4b581d9c3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 90602fa2c0e..9eb068188c7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 bc46128a9c9..fb690cf2693 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 19896f3e5d6..ac4780fa50f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 e249286364c..6d0198b9b6a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 8450924573e..2ea5793519b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 3aa32fc8d3f..88d32e67d1d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 2dbf9fec5b3..b5dec27ab14 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a5ca54c6d42..f5f1a42c141 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 44de88f3823..4fc2a9a6a70 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5cfb4c0388a..a2b045ea274 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8ccbb539e5c..3c93c5850f6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", 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 fec627b879a..301aba46f38 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 298adc1fd19..a5b78204514 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f79bfbbc9db..250a9e128c3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5b2fe3a5778..e7b9015c59d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", 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 8fecc94c5bc..501976ecc14 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a2f96d210d4..8fc3fe1fd41 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 692967ede25..fda5a92cfb4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 22dd9acc369..015d2cc0a13 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 742924867bf..8b53cc35b21 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 index d284eca5984..e04c8b3ee7e 100644 --- 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 @@ -3,7 +3,7 @@ "family": "session.markdown-disk-fallback", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", 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 index d070a4045a1..e34993cefcf 100644 --- 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 @@ -3,7 +3,7 @@ "family": "session.markdown-disk-fallback", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", 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 edd06be0450..f9d5c2d6778 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0c86e7a752c..63577bc9c28 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9145e1b22ca..b4aa358c816 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d55f04de28a..9b429d1aec6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 70d62a56811..88889306408 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c1baafc684e..ec3e90d6492 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f4be308503a..7c71d3750a3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ca1f9ce13db..efed2de7e0b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a51a2d5dd12..4578ecc3ad3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 3cae83bcc9e..801140db727 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 16d9172aae1..24a2b9b0d4c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 8e9c61ff5a7..7c9de5929c3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 db448856290..5537e82363f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 37076b4d27b..7102357f0fe 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a8e8b1fda9a..c05867fa10f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 096afff3ad4..edb26846e0d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-agent.launchreplay-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-agent.launchreplay-1.json index d8c4acda08a..7cb300eb411 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-agent.launchreplay-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-agent.launchreplay-1.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-preflight.detectagents-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-preflight.detectagents-1.json index 49674829a77..fcb3330b818 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-preflight.detectagents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-preflight.detectagents-1.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-repo.list-1.json index e509c2f8eac..d3f6bbcbd6a 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-repo.list-1.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-settings.get-1.json index 5aa2ac949f8..793cb1435cb 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.pr-triage-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.pr-triage-settings.get-1.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 202974916ec..32770529f33 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ddce30f6207..79dcf7b791f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 98445d79d7c..a15faee5c95 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 aaecef28a47..90b7d342164 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f1ed4ede349..add506aecc9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", 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 adab151675c..d79b2df09c7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", 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 2088f1a6873..affdd0974f5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", 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 c8a098ebd1d..8e429ea6b72 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", 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 9e73c794475..e6122841e48 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9cceccbbca9..4e2a55ecc11 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9c18b6bdac5..7c127588951 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ba95e6b4929..8c00e200a5b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 47641477c4c..6cfee9aa3e2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 977ff3841f0..77844549a7c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f47e18d311a..80c376c3ea6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a5885732844..64a564d7fa5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cd84ae0abe6..c6bb3eca2ee 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8b0befd6e0d..be97568a3b3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 404188bcf84..3d0e30b909e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f61034b9179..cf8cb3ea08b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 44dab067c50..a91abecbfec 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2017c3d2293..9a73b8cb753 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ea57143fce5..5e7fe880f0c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 32d753e4d37..f50d92b6500 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1fc9c0d763d..b1214f83f76 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c31a784f8fa..9d938aafb56 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d22fb0fdc64..7c10f98cf15 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 506014b1006..fd21bdcd9e3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 286180c735b..1f4bcaf11fa 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 95f4b417020..2b9f88e9767 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 60cfe697dbb..5fc76824eb1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 397713fe126..b1a05557a62 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4e767dd1a33..aed8cd2f466 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cea0626169f..58eaf3c62c8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fd1810fc815..019b7bf7860 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9da2716daf3..b29402a60a3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 03876800294..b513a25ec2e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 00fbe373437..3357d1ad059 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4ec63277e7b..697bd7a56dc 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 60b06fdff09..5271ca6e711 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 62132561c94..1fa58694ab9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5451b05d18f..0413b5312f0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 89782795daf..781c4453fb3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b73c3a1f849..ed8b968d466 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 75d9f21869d..22f64f961aa 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 07523015bbc..850aad0c45b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e09d27b0474..4cc0de8e3c1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b7ebb1568e8..210ae740e67 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3e2592a3ac3..18f97a13d2b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", 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 8dd1dca24e1..58aa1c29348 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", 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 ed55d3d1d92..f105cd29f96 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", 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 52ac338f82f..4680068df63 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 85b57c953e8..4e83e3a031b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", 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 0dc0ce5cc96..4f1e509abb0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4cb11452989..c268b5e77fd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 66a4bef76dd..981d619a412 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fefa73e4d26..627a85f4dd2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 473d944a426..39f6361a0ea 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a2043d11052..2cf32bbcfd4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0ef8104890f..9178ad4f5e6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2d1d3bf86dc..f8027c550d1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7aeed9bed04..aadc58bbff9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8efd8dec7ec..96657659b8e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 02f6a42e0c0..0fd556516d5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 77d12ae27d1..8f41fad9637 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 16ffdd39382..d537ef9a273 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cae4f6c8a6c..abb97747284 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 b9510ec635a..8e9554098bb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 52239937689..1b59a18276f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 ff3ed7f91ec..8db821475d5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 dcab290e4d8..8f8cdc5dd82 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 f89d9e16871..5fbae8a9192 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 b2735a96daf..fb85e5e7d79 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 8423a565108..c1fd7934ead 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 09a5deb80f1..ef4e546b36f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 4407db97ff3..5ac35183373 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 aec93d1a133..f6ddc37e0f7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5d4e010a551..9a921f60ec5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 75e7284e2ac..66661fdfe92 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b9b3901bf1b..fb73ddfdf91 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b91a7a117db..9d268655d89 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bcdc8c57017..e898469e765 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3ea4b1565b8..a1967b96b5b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1f503351765..d0aa49fbb85 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cebe79fac8c..b13a5344e8c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 479b6bb3cb7..82147afb79a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 692c92b4a95..83739928034 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 217259e6eea..0bf04e34736 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7ecd3c1f64a..8a725b43c2b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8598bb2eb90..f078de63746 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 091a41ca2be..07caea1146c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 993dcc5bc05..ad5b2516e9a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0365dac61b3..93e2a9c0138 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 54bb28c24d9..a3190dbd8f3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d942b505dd7..59f6fc744dc 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 262ac06aa9f..7e924dee560 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3706568bc11..aa1f2f09e61 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9e9f3fdc969..8af1b976c2e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fab6368f1cc..22163e9d2eb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 201345362fa..3ddb6d68b53 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 de8d7b29d9c..93102932341 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2313bcada88..ff43beb630c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e63fb9d9ec3..58deb7a6c84 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3dc191ea236..79dc781dee0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 42a8a9da83e..e4e48b2582c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8c86d3699a3..21bd3a72283 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c890e39d3b5..5728d801bfd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9e03b251fad..b466b490213 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 931ea2fade2..810768d84b2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 63717318402..400112c00cb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 dc0d62a270b..11d0eab7074 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 73bc0b32d4e..427f4b93c04 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 74ea4e9fe24..26c3fe0e2cf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ebf2dd2af6f..9747f14435c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d48ef4e13a8..049aa7f2a32 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 52bdefcb2e6..6e56719868f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fe298fad20b..75a2aab11dc 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a103ef69b39..869b299858c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d44e1706034..2a8b844acb0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bfe8fbaa096..64bff9f06bb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 839d2b79d00..eed0fd8f95e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3af7b7c0fcb..2683eefce2c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6952ea391c1..82a9c965719 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9443a4e4e59..4720e2300ef 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c9b7ebe5dd9..d20f5e2f7ef 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9ca91ae714f..f44b7d8524d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7f2ccefaff3..1dca0b1ce70 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 95a5d3f8d70..a45018204e3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e2ca43b1f7e..fcc8971a6a8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 054bbe2bbe6..1d277c901f8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 343ed063a81..100f1dc72ff 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e7063174ca8..dcec771e5a3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e6e41eb79ea..2924a6e6c71 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 49d83e4c5e1..6a14216104f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c9d08c7b490..1da8e7e377b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 923aaa358eb..0b8309956a0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 edcc7215424..3f3b07d7f99 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 522877d7d81..c81dbc72713 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b52daff4a4f..743274c20be 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bc67329ebf2..244c36f18be 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c05895e52dd..8ce3d502001 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 05f4e492aa9..dc1517e6e32 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1458e3fccf1..02beb58f0f6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 432ad890d8f..8831bfb8fe8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cd475623810..307bc0427cb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c953fc2c0c9..1e59f4bf470 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 75c390b3017..bc6016bb0f9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4b9a718cf8c..1af1d818b40 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "23f974df8b57c723069605de1db80c339ead286b18aae38e67fce03ea50c5180", 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 d0c8375d5ff..8eed4e13824 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 be5a8106e91..dbaf0bfb521 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ab65ad2d6dc..3b2700086f9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 521c5b19610..16af661d49c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f810797ad0e..b73d10a73ce 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 21c3b6102d0..a6d36c306f8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 14d72690a4a..ff972eb7449 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4bcf523af7f..35df73656d4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5c832f18109..d50335aecc3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 96d94fe89b0..cb10520c34b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 69ec73d4576..ca43c4fd653 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fd06d7c167c..af56926182d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bfa64ad35e6..75aeb3a962c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f94ad51fb20..b4254656d45 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 33370383b41..361b70033cd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e946c8f2198..53479f698f1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3c600719aed..2b77965536d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 15bbff4736b..9af3002abb7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e90ddc05134..cab57378ab5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5f61f25da60..b05d49d4a86 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ac44344dfd5..337f7141d89 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e3305afb2d6..59e1a75e76e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", 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 9bd2e8592be..9a2cc35f7b3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", 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 e88711a965a..160e170c605 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", 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 e7c7180a762..6754149bda7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", 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 231c885875e..456a924c62d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", 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 fd19dde7c14..3cf51bfd2c9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", 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 8b0f00d34da..d8477f733f8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 130b385ed11..f8477b7cd30 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 987029285ea..cfcc4ff6da3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7d12b01799f..e7cc088517f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1c78ae16d26..9a804d66f48 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9ea63e63408..4526333c823 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5ca6a391527..286c7314ef1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9b9e360cde1..aae6c3f6d6f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 849ec115f43..e2021615a4a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 96a309cf8cf..2c454d91e5e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 99471df513d..069ab19c7e4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 70046048638..a2038eb0584 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f00cb36ffe9..ab22f058cd5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 db230f77b3c..fb04070a8f9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 index c11ee140d4f..019d9b15aad 100644 --- a/mobile/rpc-foundation/goldens/mobile-web-bundle-build-changed.json +++ b/mobile/rpc-foundation/goldens/mobile-web-bundle-build-changed.json @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-fetch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", diff --git a/mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json b/mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json index 70190efa364..c84cabb898b 100644 --- a/mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json +++ b/mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-fetch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", diff --git a/mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json b/mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json index dcc0762fae9..53e2810822b 100644 --- a/mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json +++ b/mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-manifest", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", diff --git a/mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json b/mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json index f73605c72b0..ec443d06254 100644 --- a/mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json +++ b/mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json @@ -3,7 +3,7 @@ "family": "mobileWeb.bundle-fetch", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "af339fef2c684d5709c6d3f279e5f0d9c33d17b6d4e5c89e501963400901b564", 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 f934aa28847..5750aa66cf6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0f39c92ab9d..dc80fd6650e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0b8bb809580..fc45d428da4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bce6493283e..044c0be5332 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1074bd2f390..94da28329da 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7115e1b0ea1..9aa0b7cea3e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cf18ef24a04..1f339ad76fa 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 42628ce2bd7..95586c30d05 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 eed5822bd38..bf8084aa951 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fdbf4e6cb29..8eaf7296068 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 dfea737f9e2..cd84dadae00 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 82e7ce1df09..2b14e1cf717 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 63174dde0ff..697560982a8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bdfaaf078f1..9df01ea4568 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 069b6d69e2d..f295cb8c9a0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5c62652b35f..4012a927f30 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c6404815bb3..6c2ef2da623 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 780a884a188..296d4d7b349 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0838364830d..1366bae9a1b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5228ed6c3a7..e2e98cfebd4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b3b7b21e1d8..bb10f158648 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5ef0e31f3c4..8653789f0e5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d8d9ad5cd33..8b9726edd6e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 927f2e222e2..d5232fa46cf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b07cb0d02cc..a456b535003 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 71b41beb8e4..6c076c3555f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b48df7e7da2..4cc144feba8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d1f2d80cd58..ec67928b983 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "eacf859143588ae6bee2804975d642b6c1088d57ae77ffe298620250d9a9f0e4", diff --git a/mobile/rpc-foundation/goldens/notifications-desktop-stream.json b/mobile/rpc-foundation/goldens/notifications-desktop-stream.json index eb1dc29e84d..5ae2f8256bd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b2b379a3fdd..64e2c8c1cd9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "f55275c268e49679b37d72ce6cbe71186ce02c77fe3fd53f0f21e796502d62e8", 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 6f6eca1bd32..e255279ddc7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "f55275c268e49679b37d72ce6cbe71186ce02c77fe3fd53f0f21e796502d62e8", 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 edc61beb60c..6495277b7c7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "f55275c268e49679b37d72ce6cbe71186ce02c77fe3fd53f0f21e796502d62e8", 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 d705ad25f9b..16673d73533 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "f55275c268e49679b37d72ce6cbe71186ce02c77fe3fd53f0f21e796502d62e8", diff --git a/mobile/rpc-foundation/goldens/notifications-push-gateway-rejected.json b/mobile/rpc-foundation/goldens/notifications-push-gateway-rejected.json index fa92cf8b902..4a85cc3448f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "2e3d939dc162dbc5a38d8a7207111688204a825fd70348721917b3016e1c9470", diff --git a/mobile/rpc-foundation/goldens/notifications-push-registered.json b/mobile/rpc-foundation/goldens/notifications-push-registered.json index f49f196bb93..85817d1e175 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6d9e71fefc6..92c3d0be569 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 24e83a8eb97..dbaade09b51 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 d662e971182..703f2d44b8a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", diff --git a/mobile/rpc-foundation/goldens/pr-branch-identity.json b/mobile/rpc-foundation/goldens/pr-branch-identity.json index 68711e3bc42..8b55717c760 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-branch-repo-context.json b/mobile/rpc-foundation/goldens/pr-branch-repo-context.json index 4c1f18b5826..fdffc14bf9e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-comment-mutation.json b/mobile/rpc-foundation/goldens/pr-comment-mutation.json index 98fb81fb43e..5135bf25068 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-comment-resolve-unconfirmed.json b/mobile/rpc-foundation/goldens/pr-comment-resolve-unconfirmed.json index e4d974974f7..b51583c5b23 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 80210632f25..bdd5c5f00b5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-mutation-status.json b/mobile/rpc-foundation/goldens/pr-mutation-status.json index 9df9ebbc08f..5b9696a333c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-read-fork-routing.json b/mobile/rpc-foundation/goldens/pr-read-fork-routing.json index 82ae04bdf19..9c63ea6f1ac 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-read-surface.json b/mobile/rpc-foundation/goldens/pr-read-surface.json index beb3caed1aa..000138b09f7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-read-upstream-error.json b/mobile/rpc-foundation/goldens/pr-read-upstream-error.json index 921b277e405..ac5d882d2d5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-sidebar-checks-refused.json b/mobile/rpc-foundation/goldens/pr-sidebar-checks-refused.json index d02fb4fe598..0f676a3ec3c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/pr-sidebar-load.json b/mobile/rpc-foundation/goldens/pr-sidebar-load.json index 0a6b876e203..b620dbb5ff3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "87ffa2daea415d2682f1112025b5bd9d52404c6fc7c239180a3ad2120678ff1f", diff --git a/mobile/rpc-foundation/goldens/pr-title-mutation.json b/mobile/rpc-foundation/goldens/pr-title-mutation.json index 3025207a91b..8b70312a91a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-title-unconfirmed.json b/mobile/rpc-foundation/goldens/pr-title-unconfirmed.json index 76543bd7347..f3925091c1e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-triage-launch-unconfirmed.json b/mobile/rpc-foundation/goldens/pr-triage-launch-unconfirmed.json index 541634775ca..dd9dc7f9932 100644 --- a/mobile/rpc-foundation/goldens/pr-triage-launch-unconfirmed.json +++ b/mobile/rpc-foundation/goldens/pr-triage-launch-unconfirmed.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-triage-launch.json b/mobile/rpc-foundation/goldens/pr-triage-launch.json index e98c39ee06a..6409255dde3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-triage-prompt-not-delivered.json b/mobile/rpc-foundation/goldens/pr-triage-prompt-not-delivered.json index 093aa0d0036..a3a89906998 100644 --- a/mobile/rpc-foundation/goldens/pr-triage-prompt-not-delivered.json +++ b/mobile/rpc-foundation/goldens/pr-triage-prompt-not-delivered.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", diff --git a/mobile/rpc-foundation/goldens/pr-triage-update-required.json b/mobile/rpc-foundation/goldens/pr-triage-update-required.json index 2abc1f20f77..9c177b4f7d8 100644 --- a/mobile/rpc-foundation/goldens/pr-triage-update-required.json +++ b/mobile/rpc-foundation/goldens/pr-triage-update-required.json @@ -3,7 +3,7 @@ "family": "session.pr-triage", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5f65c5b318f4764b0f58a5e7b593333759a9cfa799db45b11190ac26f4083335", 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 3a0831c480e..5fc9131610e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8976f58317b..7f025ba6705 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b4fe98c09e7..c35f9f15bb4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8dd63775a20..ead340e9da1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f4055dac19a..efb5cb44258 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4b66792f880..9b474b2fe94 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 db28cbb0b9d..f0a3927eb9d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d2fd843f5b1..f35f31ef011 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 86fff0ea1fc..128b5683855 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 cbdc1ed6758..49bb0d6916b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 e9f548846ff..93d35123826 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 462f491de85..01ea2c1dd1c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "57c8ad93d162ea69e4a0ecd5de7d8fae6806f8f7a51945c2c13661d81bae6497", 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 516a4c0e51c..df749f8a8ad 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", 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 c1ba267c15e..9b00ed66ae0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "374a16da4187ee44fcc6e07daf822ae96ecfc13ab47ee6c29d048b1c0bd0f291", diff --git a/mobile/rpc-foundation/goldens/review-branch-diff-shapes.json b/mobile/rpc-foundation/goldens/review-branch-diff-shapes.json index 59f4d1f8fa6..111141a241d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "a8016eb61915cf80a3bdeb622ee67d35be8b4b9862a75e4ef2e8f4ff8e93e7f2", diff --git a/mobile/rpc-foundation/goldens/review-create-agent-refused.json b/mobile/rpc-foundation/goldens/review-create-agent-refused.json index 46eacedfb03..4b3e87f2df7 100644 --- a/mobile/rpc-foundation/goldens/review-create-agent-refused.json +++ b/mobile/rpc-foundation/goldens/review-create-agent-refused.json @@ -3,7 +3,7 @@ "family": "session.diff-review-actions", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", diff --git a/mobile/rpc-foundation/goldens/review-file-diff-shapes.json b/mobile/rpc-foundation/goldens/review-file-diff-shapes.json index 52d497f4ff6..468c34e78f7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6c7223e8284..acd150c9b2a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", diff --git a/mobile/rpc-foundation/goldens/review-mark-reviewed-persists.json b/mobile/rpc-foundation/goldens/review-mark-reviewed-persists.json index 23eb48c754e..b85c016c15a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", 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 38014ac78ee..e99516ff777 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", diff --git a/mobile/rpc-foundation/goldens/review-open-in-session.json b/mobile/rpc-foundation/goldens/review-open-in-session.json index b76d69a3b93..569e83371df 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", 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 102af74f4ab..a05b6062e16 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", 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 6ef0285d606..97ad2e2b3c6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", diff --git a/mobile/rpc-foundation/goldens/review-stage-file.json b/mobile/rpc-foundation/goldens/review-stage-file.json index 1e943bc5d44..c26300ecae4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", diff --git a/mobile/rpc-foundation/goldens/review-stage-refused.json b/mobile/rpc-foundation/goldens/review-stage-refused.json index 0a1e6a8a534..ba13c5d523f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "dc3a746274b4197b02b0b3060bbf8ab0f67eb54355c8b9888e1f061e37f777af", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-default.json b/mobile/rpc-foundation/goldens/sc-base-ref-default.json index a01f9f2b924..5942b67df03 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3bf23f0d0ba..b8452d82410 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8c8eeb7fd8c..50df2e2ddab 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 02b22c13ae7..ffee9e6aadd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 fd257bbff10..7b1b824e2bc 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/sc-changes-loaded.json b/mobile/rpc-foundation/goldens/sc-changes-loaded.json index 898491e315a..e958945db16 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5243f30ae1b..2f9d956fb0c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 82c4dc0b73b..154bc9c8cc1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 50e21b49451..8afa223e0f8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d924c7310d5..d2ba7926178 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9361f82b528..c6b49807d94 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f7e1ffd4130..eea93b6b063 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 12d758d9b8b..f5976e7eec6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 53a865285b6..d2a7744e237 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 95754691012..3500b4a605a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 be82322d1d8..4246171faf0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json b/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json index 5b8de2f15ef..f9cea076c93 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cc1728d3d82..650632d4a9d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "48ccada93f208a24160483e98ee94a771ab6d63222f29ac4bbd6979157f98333", diff --git a/mobile/rpc-foundation/goldens/sc-history-loaded.json b/mobile/rpc-foundation/goldens/sc-history-loaded.json index cefe8dec69e..6ab05a8ff9a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 52f6a72ef9a..dc8de943cb4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 baa447adcc8..ebb5e7ec15b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b03e46c0934..34f35559c0f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e450baf4fe5..31773808f1c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 22e03e7ae84..d526b09470e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c2be3714397..1dbb4649be9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json b/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json index 1782c716e34..18a8b027d3b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-push.json b/mobile/rpc-foundation/goldens/sc-prerequisite-push.json index e57f5e8fbd7..319b4979fcf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json b/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json index a6a3ccd115a..6d82311a78f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4fbc890e10a..a8adca2c4b9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", diff --git a/mobile/rpc-foundation/goldens/sc-reveal-timeout.json b/mobile/rpc-foundation/goldens/sc-reveal-timeout.json index e529b87d415..ab99918545d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 02bbd1d4f4a..e6aa5a4faa6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c4077e2331f..27caff58614 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 00dfc906589..434e9f70cf7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/sc-review-commit.json b/mobile/rpc-foundation/goldens/sc-review-commit.json index 523af3afd4f..da0aa2e6a9f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a39edfc8cf8..f63660a3cd9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1f1730fbf81..7b5a556643b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", diff --git a/mobile/rpc-foundation/goldens/schedules-b3.json b/mobile/rpc-foundation/goldens/schedules-b3.json index 38674f2cd5b..4f853b2b96f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 90ad47f260d..bea3dafeabd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ecdc4157c67..68d4feddea3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8bf76be19f4..ddbc2f475c2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 659e9c9623d..4eb88063ebf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a20253e0834..b2d3830f925 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 962706318c8..66a95b727a5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b9b271def77..b04cda06749 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5bef4c17f0a..1d77a1a3c6a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 744c50b126c..9e18d6c81d8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 958d728be47..5a53b692c8e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 13afb839289..af1f1da0e63 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bc49333a750..db66770d615 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a5aa4d693f8..f78621dd39b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ac3b0146083..8bc650492e4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b5354c9a885..d019905f012 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 33c77734018..5dec9e36cef 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4d244639058..a279744b04d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 79dfa0e70c6..382ae3b6658 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7c3df89b6a4..44072a16f7b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7e41166ae67..486e22ee91f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a7bb8ff898a..43ee9a7704c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 eb7eb1cbc5a..1464aee2675 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-markdown-disk-read.json b/mobile/rpc-foundation/goldens/session-markdown-disk-read.json index 4d42d84c8a0..33906f96031 100644 --- a/mobile/rpc-foundation/goldens/session-markdown-disk-read.json +++ b/mobile/rpc-foundation/goldens/session-markdown-disk-read.json @@ -3,7 +3,7 @@ "family": "session.markdown-disk-fallback", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-markdown-disk-served.json b/mobile/rpc-foundation/goldens/session-markdown-disk-served.json index 5405ef59f38..aff12975952 100644 --- a/mobile/rpc-foundation/goldens/session-markdown-disk-served.json +++ b/mobile/rpc-foundation/goldens/session-markdown-disk-served.json @@ -3,7 +3,7 @@ "family": "session.markdown-disk-fallback", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "a3daf9a05d05424959baf77feb45cb0c076d77d8c49e877941ed612cc1e5ce95", diff --git a/mobile/rpc-foundation/goldens/session-markdown-save-conflict.json b/mobile/rpc-foundation/goldens/session-markdown-save-conflict.json index dadd4338f0d..49b8f03cfcf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42334358b5e5966001639653b553f15033f6e201d785107871fe056536f0a5e2", diff --git a/mobile/rpc-foundation/goldens/session-markdown-saved.json b/mobile/rpc-foundation/goldens/session-markdown-saved.json index 7572025304e..a55ed7cac7b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 35e69380af4..d4f03e527ca 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3aa03329f50..b9426e53cb4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0eaf1fe08c8..db44a855f00 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f42c64c4432..05e12d59cd5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f2dc1d61fca..9b75f0a29ee 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f877cbf30bd..dc0af2b1c43 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0d7223f52f5..9ba927b4a88 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e318448da70..90fc3c39d9a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b568ef3c28c..632862d8281 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 840aeb36c5f..54bac0ec3e2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d7965799fb6..c567b3dff9a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a0c45cf4c9a..f1ad9234d97 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 51951642c40..f513fa07b59 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tab-closed.json b/mobile/rpc-foundation/goldens/session-tab-closed.json index d69e9d539d5..d68d1c2fe1e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tab-rename.json b/mobile/rpc-foundation/goldens/session-tab-rename.json index 666d9de75f6..9c38aa6d43e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "562c2f4ab669e774dd7045ecd9518e483845433fef6034688090f3b056df2815", diff --git a/mobile/rpc-foundation/goldens/session-tab-renamed.json b/mobile/rpc-foundation/goldens/session-tab-renamed.json index b67716fba42..ff8aab7dd9e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 099094e7c06..592720b6ebf 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6f608d13645..43513fd5d5f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 952c1fca8cf..646aeedd242 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0d3f2650bdb..1b38eea06ba 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8bb2d688a1a..eb941532060 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4122e8803ac..22c345648ec 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8240ec9c4a9..b2ed727567b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 08b6c1b0cc6..9d8e0ca1fda 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 732087cdc44..3bec41d3d14 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a076ae6cc9f..819c744595e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b7d2c3e2eed..68a8101eb70 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3a893faff39..dd47dfd4a3f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7d034cea715..702e90b09d9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b1b174597ab..10aa9bf6fad 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e1f4e3224ad..cbc26766aba 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6d281b4f23b..59f48eb7903 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 342ac6c4620..af9092a191a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-home-coalesced.json b/mobile/rpc-foundation/goldens/settings-home-coalesced.json index 4dd791f1a90..bfd248c1111 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 90cb00d86a4..8559c33d6f8 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f1c9d5f27cd..2383aa11981 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 142f90c69be..09641d9d31f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2b5c55a18bc..7e0cfa9e465 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0a6362bc82d..7678703497f 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 dbdb53084d8..5a83681082c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4306706463b..a14c158b06b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 30457d0762e..eb4fc941aca 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 523da424577..48d11dcd47b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json index b41a07f9ffa..f2d0e0648ed 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json @@ -3,7 +3,7 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", 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 9116d51c643..534db6719a7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4681e027614..826fa175c36 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 656207c38f8..3ff00bc0066 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c02040b8b0f..b096dc378ef 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f339231a881..a3fde54d0e0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d6beaa00054..70edb4661de 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 77d6d3713ca..3daae99cf55 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3deb52b2b44..995ad34d24b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e634538fa16..9940a7b242c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 83efbfab371..a2353d9cd01 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c0984fe9e71..ef29c952cfc 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 898195b8983..b03484b95bc 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b7df634d490..9331d8c3eb4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ae87f0db151..9088f5d3b1e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b24de99905e..703829be684 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b70fcbd4149..5e433195d9a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 849918850b6..1575042ffff 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "c2eed306311a844cd6f2f84b6513c0a1182f86a5e3cace434385b8287c80d7c5", diff --git a/mobile/rpc-foundation/goldens/settings-task-write.json b/mobile/rpc-foundation/goldens/settings-task-write.json index c2427e4abd2..c0aa3ba1b23 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9fd051522b7..647269388f2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 26fd43778bf..5dad315f94d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ff80fcec21e..5abf97f8206 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cb3847b4bec..d7262a8d933 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 77dee7e13d7..c14e5708c0c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a05ffe0f8ec..37a2b5ef9cb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5249b50344f..5d7cc485d6b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bbe5dfede8f..6a67d75a0f4 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", diff --git a/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json b/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json index 87c4248cd47..67ed20e3710 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 38c3993f7c5..834ee80e8e1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", diff --git a/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json b/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json index c412fe2e6ba..b95b912f28e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", diff --git a/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json b/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json index 2e9f7373bff..7f0ff48fca1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", diff --git a/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json b/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json index f850611e6c2..7f5f2e8ec5d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 fb2d8d4f7ce..76559248ae9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", diff --git a/mobile/rpc-foundation/goldens/speech-setup-sheet-fulfilled.json b/mobile/rpc-foundation/goldens/speech-setup-sheet-fulfilled.json index 2195bfb30d7..2148cd20762 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", 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 30e2fffee62..73973586c8b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", diff --git a/mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json b/mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json index 7ac10312b69..224c6e49d73 100644 --- a/mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json +++ b/mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json @@ -3,7 +3,7 @@ "family": "speech.setup-sheet", "namedDeltas": [], "runnerVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "5d978690e3626892b83bdba64537f9b1e1bb66c2060a00517ceead042a6d2334", diff --git a/mobile/rpc-foundation/goldens/structured-agent-session-created.json b/mobile/rpc-foundation/goldens/structured-agent-session-created.json index affd710223a..dd1bf6e99a5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/structured-launch-created.json b/mobile/rpc-foundation/goldens/structured-launch-created.json index 37241af0e8c..bf2a2e0f42e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 25ba8d1df89..0d19d85ec3b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 5e88d8eac71..2902789d8c9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f43915d8e83..e73320d67e3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "d340697a64a198066b1037a550afb9a0de7507246a545901d2fc0a23407f38d6", diff --git a/mobile/rpc-foundation/goldens/structured-launch-unsupported.json b/mobile/rpc-foundation/goldens/structured-launch-unsupported.json index f7faf6b482d..1a87bc4c4ff 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3e084c448c7..290bf880385 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "23f974df8b57c723069605de1db80c339ead286b18aae38e67fce03ea50c5180", 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 53f0662a808..2029b150b96 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 af9abae2cc6..90d2e5d7b69 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b56cd3f6394..fc3cbf9ceb1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2dc412990ce..e4cf7f75331 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/terminal-paste-accepted.json b/mobile/rpc-foundation/goldens/terminal-paste-accepted.json index 63c315bedae..a11282a2378 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/terminal-paste-refused.json b/mobile/rpc-foundation/goldens/terminal-paste-refused.json index 49d2d72949c..6ee741a85e6 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 267c1fba6ee..af4b89b745e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", diff --git a/mobile/rpc-foundation/goldens/terminal-query-reply-unsubscribed.json b/mobile/rpc-foundation/goldens/terminal-query-reply-unsubscribed.json index ff7f371a4c4..2a4d6c75152 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", diff --git a/mobile/rpc-foundation/goldens/terminal-raw-input-refused.json b/mobile/rpc-foundation/goldens/terminal-raw-input-refused.json index 26ffb508c4a..c8ec6a1c4c1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", diff --git a/mobile/rpc-foundation/goldens/terminal-raw-input-reported.json b/mobile/rpc-foundation/goldens/terminal-raw-input-reported.json index aee36c3ce7c..daf232e0867 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", diff --git a/mobile/rpc-foundation/goldens/terminal-takeover-report-accepted.json b/mobile/rpc-foundation/goldens/terminal-takeover-report-accepted.json index 123147a10b4..cd7ab9ab2df 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", diff --git a/mobile/rpc-foundation/goldens/terminal-takeover-report-retried.json b/mobile/rpc-foundation/goldens/terminal-takeover-report-retried.json index 00650466139..059ef635246 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", diff --git a/mobile/rpc-foundation/goldens/terminal-viewport-refit-applied.json b/mobile/rpc-foundation/goldens/terminal-viewport-refit-applied.json index bfceb4f8565..581b27eb031 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", 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 d2517fbbb0d..c60bedf21b2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "42e1fe434b591ab5a945b6b366bbb20b567085d58bade63413f2251e8202b255", diff --git a/mobile/rpc-foundation/goldens/terminal-worktree-connection-resolved.json b/mobile/rpc-foundation/goldens/terminal-worktree-connection-resolved.json index ffbc1b58c98..73dda3c865b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "e7e3718f685e3713cf1b8209d59d618741f892bd286853b79bb456c59cec8d86", diff --git a/mobile/rpc-foundation/goldens/tk-create-github.json b/mobile/rpc-foundation/goldens/tk-create-github.json index 9830170d67b..97b68c2b786 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-create-gitlab.json b/mobile/rpc-foundation/goldens/tk-create-gitlab.json index 8f1f8a62a98..9e8c31c0a14 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-create-linear.json b/mobile/rpc-foundation/goldens/tk-create-linear.json index 2a9126f46c3..cbe96ac449d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 114fedc21e0..4cdfd0c51af 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 937620fb171..2e0bdd7a356 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 11fec15f8f1..058313a2328 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8a390b7186a..3ebefe16c04 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b3d2c0fd311..3e5c3a69a39 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6a8dfa8fcf1..2cf15710591 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7588e2b5574..1e93f61396b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 57bb99c0af6..6ecbce878a2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 02a387d6fd0..95534639bb0 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 bd56fe66b9d..44881318422 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f5ff4f3bfe2..9f8de155696 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e8db3fab93e..b6da88a9d85 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e7574077a8b..ea380eb5a05 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 75f62fb2bc8..810e824ceec 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c20fe0cd3ca..c62f847e112 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 13691d47856..4deb1f092d1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 589b2509aca..b06650daa5a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0575f6fb77b..4b85268372e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "8c4218bfb2af227da5386f29989cec438f2c6187f39ce1c06859e136ea920bfa", diff --git a/mobile/rpc-foundation/goldens/tk-linear-connect.json b/mobile/rpc-foundation/goldens/tk-linear-connect.json index 88ed753809b..846d956b820 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-linear-item.json b/mobile/rpc-foundation/goldens/tk-linear-item.json index 2be2f8b7b8b..a53edae9599 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f95a5921fb7..3150b00e928 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 17075a6ac7e..9c91fd929cb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 105030cfffc..579f12b4e0c 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "07a15e900363faa25bc364f5bc1c330f96798abfeaed35210428c2ac558586b8", diff --git a/mobile/rpc-foundation/goldens/tk-list-linear.json b/mobile/rpc-foundation/goldens/tk-list-linear.json index e59427608a0..4d092d51f28 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 34f6c223cb8..943dbd2171a 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a8da1a688d3..81ffe92128b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e0551419589..949d1dbdbbc 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4dc462a452d..5c9d694994b 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 77e96fd0a80..044028aef21 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4b6aca5c43f..55ad5516365 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3ce1d9fe984..67a69ea0393 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 18af22344d0..3904cef4311 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a0c52914a8f..da2395d92e5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 edfa9242eef..70ceddb8092 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "55058202df36c8b951510215936e496ea88d3d71a6690090a13c52deb13e34e1", diff --git a/mobile/rpc-foundation/goldens/tk-provider-load.json b/mobile/rpc-foundation/goldens/tk-provider-load.json index 3d3e4ad8534..7d5ac795b30 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 cfbce33331d..934a09b8de3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 59e0b1ec0d1..f204e48c500 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8cd9d218028..6fe86b71a24 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 363f217f67e..7d1c70b77d9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d1912117792..7e3d011ad97 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0d313eb204a..49826dee6bd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 daafc2f194a..a10131c2a39 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 1b02fe839e6..460002781fd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 adcfd69373f..0413371cb08 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d65876fc4ec..f1bdfadf093 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 86f3ee3ab58..51c4c340c41 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "34b382b13fe75e8ef4002325287c95b3c4db62eeaaf3762af7fbaf6f836c2fa1", diff --git a/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json b/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json index 34dd6a265ca..6cb44346bcd 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 7f8e6c35efe..9c2c0ce90be 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 3ec385f6f9d..293d4054393 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6d54ac9bd0f..06897da697d 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a339da97815..d0a290e02d5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f3a296d4038..705f673283e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 226e3704417..a9ee18f5fd3 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 328da8b4663..3d401c51a14 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8e2f0372814..62b80b0b0f2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 79d832aa93c..7704fba5335 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 2f6fb9691fe..7e8f49a396e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 8a68e2be5b0..ae16a873285 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 a5a19cb9745..f0814a999a1 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 9fa9a5c2886..9885cf5025e 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4636455e202..ee51e1f7e20 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 893cca287b6..be1437b6558 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 267496fd555..1dc9de89672 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b7502a2fbd9..29f6508a102 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 4495984796d..8efd95cbd83 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 0dcb4385826..bc0034b9cf9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6dba57e250b..732ba4a5fc7 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 52720072511..ca77155f5df 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 b65f14ebf0a..4b8acbb2973 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 e6674e63ca3..d45a2618cd5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 d908aa970ff..9a3a7e5b6b9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 f7c7b9d3509..7dc69202c86 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 6b4d1c0fcb4..58893ee5974 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 c93bbbcdef5..1683f8218d2 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 ccfccc512c3..16c4f4555c9 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 acd77a24c78..7728d271c72 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "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 44cec939c66..8ebe6cbfabb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json index c09e3cbce45..a4a8ae811e5 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/worktree-home-catalog.json b/mobile/rpc-foundation/goldens/worktree-home-catalog.json index c0b50f6fdb9..3b2f9113feb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/goldens/worktree-retired-names.json b/mobile/rpc-foundation/goldens/worktree-retired-names.json index 14cdb364616..2bbd621adcb 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": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb", "recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af", "adapterSha256": "4e942ddfbaa0ba6bfc2993969276987f6528ac53765d125e99a830e261f93a8e", diff --git a/mobile/rpc-foundation/pilot-scenarios.json b/mobile/rpc-foundation/pilot-scenarios.json index 254c36272b1..647e4dcad01 100644 --- a/mobile/rpc-foundation/pilot-scenarios.json +++ b/mobile/rpc-foundation/pilot-scenarios.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "baseline": "03995ae29d28dd39bd2546423831987db68985de", + "baseline": "ccdb324b63e4942891942edde1216586a51774bd", "scenarios": [ { "id": "b1", diff --git a/mobile/scripts/rpc-recording-pin-guard.mts b/mobile/scripts/rpc-recording-pin-guard.mts index c7b11a48495..807893dcb28 100644 --- a/mobile/scripts/rpc-recording-pin-guard.mts +++ b/mobile/scripts/rpc-recording-pin-guard.mts @@ -1,10 +1,12 @@ /** * Guards the two claims `mobile/rpc-foundation/goldens` makes about its pin. * - * `ancestry` — `baseline` names a commit in this history. A behaviour-change branch pins its own - * last fenced commit; that commit stops being reachable the moment the branch - * squash-merges, and nobody can run `--record` on main again until a hand-made repin - * lands. Ordinary product drift past a reachable pin is normal and is not a failure. + * `reachable` — `baseline` names a commit this repository keeps: one in this history, or one in + * the head of a pull request GitHub associates with that commit. A behaviour-change + * branch pins its own last fenced commit, which the squash leaves out of main's + * history; `refs/pull//head` outlives the branch, so the pin stays checkable out. + * GitHub only names the candidates; git's ancestry check decides. Ordinary product + * drift past a kept pin is normal and is not a failure. * `reproduce` — the goldens on disk are what the recorder produces from the PINNED tree. The * recording suites replay the corpus against the CURRENT tree on every run, which is * the same claim only while the fenced tree still matches the pin. @@ -17,6 +19,7 @@ import { join, resolve } from 'node:path' import { runProcess } from '../../src/shared/child-process/run-process.ts' import { RECORDING_DRIVERS } from '../src/test-support/rpc-recording/recording-drivers.ts' import { readScenarios } from '../src/test-support/rpc-recording/scenario-input.ts' +import { checkPinReachable, PIN_MANIFEST } from './rpc-recording-pin-reachability.mts' /** The recorder is exempt from the fence, so a reproduction lays the candidate copy over the pin. */ const RECORDER_OVERLAY = 'mobile/src/test-support/rpc-recording' @@ -67,90 +70,11 @@ export function assertReproductionSuitesExist(root: string): void { } } -export type PinAncestryFailure = 'shallow' | 'unreachable' | 'not-an-ancestor' -export type PinAncestryVerdict = - | { ok: true; baseline: string; ref: string } - | { ok: false; baseline: string; ref: string; failure: PinAncestryFailure; message: string } - async function git(cwd: string, args: readonly string[]) { return await runProcess({ program: 'git', args: [...args], cwd }) } function readPinnedBaseline(root: string): string { - return readScenarios(resolve(root, 'mobile/rpc-foundation/pilot-scenarios.json')).baseline -} -export function repinInstruction(baseline: string, ref: string, cause: string): string { - return [ - `The RPC recording corpus is pinned to a commit that ${cause}.`, - '', - ` baseline ${baseline} (mobile/rpc-foundation/pilot-scenarios.json)`, - ` head ${ref}`, - '', - 'Every golden under mobile/rpc-foundation/goldens claims it was recorded from that tree, and', - '`--record` refuses on any other tree, so the corpus cannot be refreshed until the pin names a', - 'commit that is reachable from here. Repin and re-record, both in one commit:', - '', - ` git switch -c repin-rpc-recording ${ref}`, - ` # set "baseline" in mobile/rpc-foundation/pilot-scenarios.json to ${ref}`, - ' ORCA_BACKGROUND_LAUNCH=1 RPC_FOUNDATION_RECORD=1 \\', - ' pnpm --dir mobile exec tsx scripts/rpc-recording.mts --record', - '', - 'Re-record everything: the repin rewrites the `baseline` header of every golden, so a partial', - 'refresh leaves the corpus pinned to two different trees. See', - 'mobile/src/test-support/rpc-recording/README.md, "Recording a behaviour change".' - ].join('\n') -} -const SHALLOW_MESSAGE = [ - 'Cannot judge the recording pin: this is a shallow clone.', - '', - '`git merge-base --is-ancestor` answers from grafted history, so it would report a verdict this', - 'guard has no evidence for. Check out with `fetch-depth: 0`.' -].join('\n') - -export async function checkPinAncestry( - root: string, - baseline: string, - ref: string -): Promise { - const shallow = await git(root, ['rev-parse', '--is-shallow-repository']) - if (shallow.code !== 0) { - throw new Error(`Could not ask git whether the clone is shallow: ${shallow.stderr.trim()}`) - } - if (shallow.stdout.trim() !== 'false') { - return { ok: false, baseline, ref, failure: 'shallow', message: SHALLOW_MESSAGE } - } - const head = await git(root, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]) - if (head.code !== 0) { - throw new Error(`Cannot resolve ${ref} to a commit in this repository`) - } - // Resolved, because the instruction below is a command to paste: `HEAD` in it moves with whatever - // the reader has checked out by the time they read the log. - const resolved = head.stdout.trim() - const pinned = await git(root, ['rev-parse', '--verify', '--quiet', `${baseline}^{commit}`]) - if (pinned.code !== 0) { - return { - ok: false, - baseline, - ref, - failure: 'unreachable', - message: repinInstruction(baseline, resolved, 'is not a commit in this repository at all') - } - } - const ancestor = await git(root, ['merge-base', '--is-ancestor', baseline, ref]) - if (ancestor.code === 0) { - return { ok: true, baseline, ref } - } - // Why only 1: git reserves higher codes for real errors, and treating one as "not an ancestor" - // would turn a broken repository into a repin instruction nobody can act on. - if (ancestor.code !== 1) { - throw new Error(`git merge-base --is-ancestor failed: ${ancestor.stderr.trim()}`) - } - return { - ok: false, - baseline, - ref, - failure: 'not-an-ancestor', - message: repinInstruction(baseline, resolved, 'is not an ancestor of this commit') - } + return readScenarios(resolve(root, PIN_MANIFEST)).baseline } /** Whether anything a reproduction reads from the candidate tree moved since `since`. */ @@ -284,18 +208,22 @@ async function main(argv: readonly string[]): Promise { const check = argv[0] const root = resolve(import.meta.dirname, '../..') const baseline = readPinnedBaseline(root) - if (check === 'ancestry') { + if (check === 'reachable') { const ref = argv.includes('--ref') ? argv[argv.indexOf('--ref') + 1] : 'HEAD' if (!ref) { throw new Error('--ref needs a commit') } - const verdict = await checkPinAncestry(root, baseline, ref) + const verdict = await checkPinReachable(root, baseline, ref) if (!verdict.ok) { process.stderr.write(`${verdict.message}\n`) process.exitCode = 1 return } - process.stdout.write(`Recording pin ${baseline} is an ancestor of ${ref}.\n`) + process.stdout.write( + verdict.pullRequest === null + ? `Recording pin ${baseline} is an ancestor of ${ref}.\n` + : `Recording pin ${baseline} is kept by refs/pull/${verdict.pullRequest}/head.\n` + ) return } if (check === 'reproduce') { @@ -313,6 +241,13 @@ async function main(argv: readonly string[]): Promise { return } } + // Also fetches a pin that only its pull request's head still holds, so the checkout can find it. + const verdict = await checkPinReachable(root, baseline, 'HEAD') + if (!verdict.ok) { + process.stderr.write(`${verdict.message}\n`) + process.exitCode = 1 + return + } if (!(await reproduceFromPin(root, baseline))) { process.stderr.write(`\n${REPRODUCTION_FAILURE}\n`) process.exitCode = 1 @@ -322,7 +257,7 @@ async function main(argv: readonly string[]): Promise { return } throw new Error( - 'Usage: rpc-recording-pin-guard.mts ancestry [--ref ]\n | reproduce [--if-changed-since ]' + 'Usage: rpc-recording-pin-guard.mts reachable [--ref ]\n | reproduce [--if-changed-since ]' ) } diff --git a/mobile/scripts/rpc-recording-pin-guard.test.ts b/mobile/scripts/rpc-recording-pin-guard.test.ts index e6154d51c2c..616034d72b1 100644 --- a/mobile/scripts/rpc-recording-pin-guard.test.ts +++ b/mobile/scripts/rpc-recording-pin-guard.test.ts @@ -1,16 +1,21 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' -import { afterAll, describe, expect, it } from 'vitest' +import { afterAll, afterEach, describe, expect, it, vi } from 'vitest' import { runProcess } from '../../src/shared/child-process/run-process' import { assertReproductionSuitesExist, - checkPinAncestry, corpusProvenanceChanged, removeScratchWorktree, - repinInstruction, REPRODUCTION_SUITES } from './rpc-recording-pin-guard.mts' +import { + checkPinReachable, + gitHubRepositoryFromRemote, + PIN_MANIFEST, + pullRequestsWithCommit, + repinInstruction +} from './rpc-recording-pin-reachability.mts' const scratch: string[] = [] async function git(cwd: string, ...args: string[]): Promise { @@ -54,6 +59,50 @@ async function seedProvenance(repository: string): Promise { return await commitAt(repository, RECORDER_FILE, 'recorder') } const MISSING_SHA = '0123456789abcdef0123456789abcdef01234567' +function pinManifest(baseline: string): string { + return JSON.stringify({ baseline, scenarios: [] }) +} +/** + * A behaviour-change branch that pinned its own commit, squash-merged onto main, with the host + * keeping the branch head at `refs/pull/7/head` the way GitHub does after the branch is gone. + */ +async function squashMergedPin(change: string, subject = `${change} (#7)`) { + const work = await throwawayRepository() + await commit(work, 'one') + await git(work, 'switch', '--quiet', '--create', 'behaviour-change') + const branchPin = await commit(work, change) + await commitAt(work, PIN_MANIFEST, pinManifest(branchPin)) + await git(work, 'switch', '--quiet', 'main') + await git(work, 'merge', '--quiet', '--squash', 'behaviour-change') + await git(work, 'commit', '--quiet', '--no-verify', '--message', subject) + const remote = join(work, '..', 'remote.git') + await git(work, 'init', '--quiet', '--bare', remote) + await git(work, 'push', '--quiet', remote, 'main', 'behaviour-change:refs/pull/7/head') + await git(work, 'branch', '--quiet', '-D', 'behaviour-change') + return { work, remote, branchPin } +} +async function cloneMain(remote: string): Promise { + const clone = join(remote, '..', `clone-${scratch.length}-${Date.now()}`) + await git( + join(remote, '..'), + 'clone', + '--quiet', + '--single-branch', + '--branch', + 'main', + remote, + clone + ) + await git(clone, 'config', 'user.email', 'pin-guard@example.invalid') + await git(clone, 'config', 'user.name', 'Pin Guard') + return clone +} +// Stand-ins for GitHub's "pull requests associated with this commit" answer. +const heldByPullRequest7 = async () => [7] +const heldByNoPullRequest = async () => [] +const unasked = async (): Promise => { + throw new Error('a pin in this history must not send the guard to GitHub') +} // File scope, not per-suite: a suite-local hook fires before the later suites have made theirs. afterAll(async () => { @@ -62,18 +111,18 @@ afterAll(async () => { } }) -describe('recording pin ancestry', () => { +describe('recording pin reachability', () => { it('passes when the pin is the commit itself', async () => { const repository = await throwawayRepository() const first = await commit(repository, 'one') - expect(await checkPinAncestry(repository, first, first)).toMatchObject({ ok: true }) + expect(await checkPinReachable(repository, first, first, unasked)).toMatchObject({ ok: true }) }) it('passes on ordinary drift: the tree has moved on, the pin is still reachable', async () => { const repository = await throwawayRepository() const pin = await commit(repository, 'one') const head = await commit(repository, 'two') - expect(await checkPinAncestry(repository, pin, head)).toMatchObject({ ok: true }) + expect(await checkPinReachable(repository, pin, head, unasked)).toMatchObject({ ok: true }) }) it('passes on a branch that pinned its own commit, judged against that branch', async () => { @@ -82,7 +131,9 @@ describe('recording pin ancestry', () => { await git(repository, 'switch', '--quiet', '--create', 'behaviour-change') const branchPin = await commit(repository, 'two') const branchHead = await commit(repository, 'three') - expect(await checkPinAncestry(repository, branchPin, branchHead)).toMatchObject({ ok: true }) + expect(await checkPinReachable(repository, branchPin, branchHead, unasked)).toMatchObject({ + ok: true + }) }) it('passes a branch cut before main repinned, judged against the merge preview', async () => { @@ -98,29 +149,115 @@ describe('recording pin ancestry', () => { await git(repository, 'merge', '--quiet', '--no-edit', 'main') const preview = await git(repository, 'rev-parse', 'HEAD') // The preview is the tree CI checks out and reads the pin from, so it is the tree to judge. - expect(await checkPinAncestry(repository, mainPin, preview)).toMatchObject({ ok: true }) + expect(await checkPinReachable(repository, mainPin, preview, unasked)).toMatchObject({ + ok: true + }) // The head sha would have failed this ordinary branch, and told the author to repin to it. - expect(await checkPinAncestry(repository, mainPin, branchHead)).toMatchObject({ + expect( + await checkPinReachable(repository, mainPin, branchHead, heldByNoPullRequest) + ).toMatchObject({ ok: false, failure: 'not-an-ancestor' }) }) - it('fails once that branch squash-merges and the pin leaves the history', async () => { + it('passes once that branch squash-merges, through the head its pull request keeps', async () => { + const { remote, branchPin } = await squashMergedPin('two') + // Only main: the branch is deleted, so the pin is nowhere in this clone until it is fetched. + const clone = await cloneMain(remote) + expect(await checkPinReachable(clone, branchPin, 'HEAD', heldByPullRequest7)).toMatchObject({ + ok: true, + pullRequest: 7 + }) + const drifted = await commit(clone, 'later product change on main') + expect(await checkPinReachable(clone, branchPin, drifted, heldByPullRequest7)).toMatchObject({ + ok: true, + pullRequest: 7 + }) + // The reproduction checks the pinned tree out next, which needs the fetched commit. + await git(clone, 'cat-file', '-e', `${branchPin}^{commit}`) + }) + + it('passes a branch opened after that squash, judged against its merge preview', async () => { + const { remote, branchPin } = await squashMergedPin('two') + const clone = await cloneMain(remote) + await git(clone, 'switch', '--quiet', '--create', 'later-branch') + await commitAt(clone, 'mobile/src/later.ts', 'later branch change') + await git(clone, 'switch', '--quiet', '--detach', 'main') + await commit(clone, 'main moved on') + await git(clone, 'merge', '--quiet', '--no-ff', '--no-edit', 'later-branch') + expect(await checkPinReachable(clone, branchPin, 'HEAD', heldByPullRequest7)).toMatchObject({ + ok: true, + pullRequest: 7 + }) + }) + + it('passes a squash whose edited title lost its (#n), since GitHub names the pull request', async () => { + const { remote, branchPin } = await squashMergedPin('two', 'two, title edited at merge') + const clone = await cloneMain(remote) + expect(await checkPinReachable(clone, branchPin, 'HEAD', heldByPullRequest7)).toMatchObject({ + ok: true, + pullRequest: 7 + }) + }) + + it('passes through the first associated pull request whose head holds the pin', async () => { + const { remote, branchPin, work } = await squashMergedPin('two') + await git(work, 'push', '--quiet', remote, 'main:refs/pull/9/head') + const clone = await cloneMain(remote) + expect(await checkPinReachable(clone, branchPin, 'HEAD', async () => [9, 7])).toMatchObject({ + ok: true, + pullRequest: 7 + }) + }) + + it('fails the merge preview of a branch whose rebase dropped the commit it pinned', async () => { + const { remote } = await squashMergedPin('two') + const clone = await cloneMain(remote) + await git(clone, 'switch', '--quiet', '--create', 'rebased') + const droppedPin = await commit(clone, 'three') + await commitAt(clone, PIN_MANIFEST, pinManifest(droppedPin)) + await git(clone, 'switch', '--quiet', 'main') + await commitAt(clone, 'mobile/src/moved.ts', 'main moved on') + // The rebase rewrites both commits; the manifest still names the one that is gone. + await git(clone, 'rebase', '--quiet', 'main', 'rebased') + await git(clone, 'switch', '--quiet', '--detach', 'main') + await git(clone, 'merge', '--quiet', '--no-ff', '--no-edit', 'rebased') + // Caught on the pull request: after the squash no head would hold the pin either. + const verdict = await checkPinReachable(clone, droppedPin, 'HEAD', heldByNoPullRequest) + expect(verdict).toMatchObject({ ok: false, failure: 'not-an-ancestor' }) + expect(verdict.ok ? '' : verdict.message).toContain('GitHub associates no pull request') + }) + + it("fails when the pull request's head no longer holds the pin", async () => { + const { remote, branchPin, work } = await squashMergedPin('two') + // A force-push after pinning: the pin is left in no ref the host keeps. + await git(work, 'push', '--quiet', '--force', remote, 'main:refs/pull/7/head') + await git(work, 'push', '--quiet', remote, `${branchPin}:refs/heads/stray`) + const clone = await cloneMain(remote) + await git(clone, 'fetch', '--quiet', 'origin', 'stray') + const verdict = await checkPinReachable(clone, branchPin, 'HEAD', heldByPullRequest7) + expect(verdict).toMatchObject({ ok: false, failure: 'not-an-ancestor' }) + expect(verdict.ok ? '' : verdict.message).toContain('refs/pull/7/head') + }) + + it('fails once that branch squash-merges if GitHub associates no pull request with it', async () => { const repository = await throwawayRepository() const base = await commit(repository, 'one') await git(repository, 'switch', '--quiet', '--create', 'behaviour-change') const branchPin = await commit(repository, 'two') await git(repository, 'switch', '--quiet', 'main') await git(repository, 'reset', '--quiet', '--hard', base) + await commitAt(repository, PIN_MANIFEST, pinManifest(branchPin)) const squashed = await commit(repository, 'two, squashed') - const verdict = await checkPinAncestry(repository, branchPin, squashed) + const verdict = await checkPinReachable(repository, branchPin, squashed, heldByNoPullRequest) expect(verdict).toMatchObject({ ok: false, failure: 'not-an-ancestor' }) expect(verdict.ok).toBe(false) if (verdict.ok) { return } expect(verdict.message).toContain(branchPin) + expect(verdict.message).toContain('GitHub associates no pull request') expect(verdict.message).toContain('scripts/rpc-recording.mts --record') expect(verdict.message).toContain('mobile/rpc-foundation/pilot-scenarios.json') }) @@ -128,11 +265,22 @@ describe('recording pin ancestry', () => { it('fails with the same instruction when the pin is no commit at all', async () => { const repository = await throwawayRepository() const head = await commit(repository, 'one') - const verdict = await checkPinAncestry(repository, MISSING_SHA, head) + const verdict = await checkPinReachable(repository, MISSING_SHA, head, heldByNoPullRequest) expect(verdict).toMatchObject({ ok: false, failure: 'unreachable' }) expect(verdict.ok ? '' : verdict.message).toContain('scripts/rpc-recording.mts --record') }) + it('lets a failed GitHub lookup propagate, never a pass or a repin instruction', async () => { + const { remote, branchPin } = await squashMergedPin('two') + const clone = await cloneMain(remote) + const broken = async (): Promise => { + throw new Error('GitHub answered 502') + } + await expect(checkPinReachable(clone, branchPin, 'HEAD', broken)).rejects.toThrow( + 'GitHub answered 502' + ) + }) + it('refuses to answer on a shallow clone instead of trusting grafted history', async () => { const repository = await throwawayRepository() const pin = await commit(repository, 'one') @@ -141,19 +289,74 @@ describe('recording pin ancestry', () => { const clone = join(repository, '..', 'shallow') await git(repository, 'clone', '--quiet', '--depth', '1', `file://${repository}`, clone) // The pin is real and reachable in the full repository; only the missing history hides it. - expect(await checkPinAncestry(repository, pin, head)).toMatchObject({ ok: true }) - const verdict = await checkPinAncestry(clone, pin, 'HEAD') + expect(await checkPinReachable(repository, pin, head, unasked)).toMatchObject({ ok: true }) + const verdict = await checkPinReachable(clone, pin, 'HEAD', unasked) expect(verdict).toMatchObject({ ok: false, failure: 'shallow' }) expect(verdict.ok ? '' : verdict.message).toContain('fetch-depth: 0') }) it('names the head and the pin in the instruction', () => { expect( - repinInstruction('a'.repeat(40), 'b'.repeat(40), 'is not an ancestor of this commit') + repinInstruction('a'.repeat(40), 'b'.repeat(40), 'is not a commit in this repository at all') ).toContain(`git switch -c repin-rpc-recording ${'b'.repeat(40)}`) }) }) +describe("GitHub's pull requests for a pinned commit", () => { + const pin = 'a'.repeat(40) + const answer = (status: number, body: unknown) => + vi.fn(async () => new Response(JSON.stringify(body), { status })) + afterEach(() => { + vi.unstubAllGlobals() + vi.unstubAllEnvs() + }) + + it('asks with the token when one is set and reads the pull request numbers', async () => { + vi.stubEnv('GITHUB_REPOSITORY', 'stablyai/orca') + vi.stubEnv('GITHUB_TOKEN', 'test-token') + const fetch = answer(200, [{ number: 22762 }, { number: 5 }]) + vi.stubGlobal('fetch', fetch) + expect(await pullRequestsWithCommit('/unused', pin)).toEqual([22762, 5]) + expect(fetch).toHaveBeenCalledWith( + `https://api.github.com/repos/stablyai/orca/commits/${pin}/pulls`, + expect.objectContaining({ + headers: { + Accept: 'application/vnd.github+json', + Authorization: 'Bearer test-token' + } + }) + ) + }) + + it('reads no pull request for a commit GitHub has never seen', async () => { + vi.stubEnv('GITHUB_REPOSITORY', 'stablyai/orca') + vi.stubGlobal('fetch', answer(422, { message: `No commit found for SHA: ${pin}` })) + expect(await pullRequestsWithCommit('/unused', pin)).toEqual([]) + }) + + it('names the rate limit when GitHub refuses a call made without a token', async () => { + vi.stubEnv('GITHUB_REPOSITORY', 'stablyai/orca') + vi.stubEnv('GITHUB_TOKEN', '') + vi.stubEnv('GH_TOKEN', '') + vi.stubGlobal('fetch', answer(403, { message: 'API rate limit exceeded' })) + await expect(pullRequestsWithCommit('/unused', pin)).rejects.toThrow( + /answered 403[\s\S]*API rate limit exceeded[\s\S]*set GITHUB_TOKEN or GH_TOKEN/ + ) + }) + + it('reads owner and repo from https and ssh remotes, and nothing from another host', () => { + expect(gitHubRepositoryFromRemote('https://github.com/stablyai/orca.git\n')).toBe( + 'stablyai/orca' + ) + expect(gitHubRepositoryFromRemote('https://github.com/stablyai/orca')).toBe('stablyai/orca') + expect(gitHubRepositoryFromRemote('git@github.com:stablyai/orca.git')).toBe('stablyai/orca') + expect(gitHubRepositoryFromRemote('ssh://git@github.com/stablyai/orca.git')).toBe( + 'stablyai/orca' + ) + expect(gitHubRepositoryFromRemote('/tmp/remote.git')).toBeNull() + }) +}) + describe('what a reproduction reads from the candidate tree', () => { it('skips the run when only product sources moved', async () => { const repository = await throwawayRepository() diff --git a/mobile/scripts/rpc-recording-pin-reachability.mts b/mobile/scripts/rpc-recording-pin-reachability.mts new file mode 100644 index 00000000000..c7ec31ccfb1 --- /dev/null +++ b/mobile/scripts/rpc-recording-pin-reachability.mts @@ -0,0 +1,204 @@ +/** + * Whether the RPC recording pin names a commit this repository keeps: one in this history, or one in + * the head of a pull request that GitHub associates with the pinned commit. + */ +import { runProcess } from '../../src/shared/child-process/run-process.ts' + +export const PIN_MANIFEST = 'mobile/rpc-foundation/pilot-scenarios.json' +const PIN_REMOTE = 'origin' +const PIN_FETCH_TIMEOUT_MS = 180_000 +const PULL_REQUEST_LOOKUP_TIMEOUT_MS = 30_000 + +export type PinReachabilityFailure = 'shallow' | 'unreachable' | 'not-an-ancestor' +export type PinReachabilityVerdict = + | { ok: true; baseline: string; ref: string; pullRequest: number | null } + | { ok: false; baseline: string; ref: string; failure: PinReachabilityFailure; message: string } +/** Only nominates pull requests; git's ancestry check decides, so a wrong answer can only fail. */ +export type PullRequestLookup = (root: string, commit: string) => Promise + +async function git(cwd: string, args: readonly string[], timeoutMs?: number) { + return await runProcess({ program: 'git', args: [...args], cwd, timeoutMs }) +} +function failureDetail(result: { stderr: string; timedOut: boolean }, timeoutMs: number): string { + return result.timedOut ? `timed out after ${timeoutMs / 1000}s` : result.stderr.trim() +} +export function repinInstruction(baseline: string, ref: string, cause: string): string { + return [ + `The RPC recording corpus is pinned to a commit that ${cause}.`, + '', + ` baseline ${baseline} (mobile/rpc-foundation/pilot-scenarios.json)`, + ` head ${ref}`, + '', + 'Every golden under mobile/rpc-foundation/goldens claims it was recorded from that tree, and', + '`--record` refuses on any other tree, so the corpus cannot be refreshed until the pin names a', + 'commit that is reachable from here. Repin and re-record, both in one commit:', + '', + ` git switch -c repin-rpc-recording ${ref}`, + ` # set "baseline" in mobile/rpc-foundation/pilot-scenarios.json to ${ref}`, + ' ORCA_BACKGROUND_LAUNCH=1 RPC_FOUNDATION_RECORD=1 \\', + ' pnpm --dir mobile exec tsx scripts/rpc-recording.mts --record', + '', + 'Re-record everything: the repin rewrites the `baseline` header of every golden, so a partial', + 'refresh leaves the corpus pinned to two different trees. See', + 'mobile/src/test-support/rpc-recording/README.md, "Recording a behaviour change".' + ].join('\n') +} +const SHALLOW_MESSAGE = [ + 'Cannot judge the recording pin: this is a shallow clone.', + '', + '`git merge-base --is-ancestor` answers from grafted history, so it would report a verdict this', + 'guard has no evidence for. Check out with `fetch-depth: 0`.' +].join('\n') + +async function isAncestor(root: string, commit: string, of: string): Promise { + const ancestor = await git(root, ['merge-base', '--is-ancestor', commit, of]) + // Why only 0 and 1: git reserves higher codes for real errors, and treating one as "not an + // ancestor" would turn a broken repository into a repin instruction nobody can act on. + if (ancestor.code !== 0 && ancestor.code !== 1) { + throw new Error(`git merge-base --is-ancestor failed: ${ancestor.stderr.trim()}`) + } + return ancestor.code === 0 +} + +/** `owner/repo` from a GitHub remote URL, https or ssh. */ +export function gitHubRepositoryFromRemote(url: string): string | null { + const match = /github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url.trim()) + return match ? `${match[1]}/${match[2]}` : null +} +async function gitHubRepository(root: string): Promise { + if (process.env.GITHUB_REPOSITORY) { + return process.env.GITHUB_REPOSITORY + } + const remote = await git(root, ['remote', 'get-url', PIN_REMOTE]) + const repository = remote.code === 0 ? gitHubRepositoryFromRemote(remote.stdout) : null + if (!repository) { + throw new Error( + `Cannot tell which GitHub repository ${PIN_REMOTE} is ` + + `(${remote.stdout.trim() || remote.stderr.trim()}); set GITHUB_REPOSITORY=/` + ) + } + return repository +} + +/** The pull requests GitHub associates with `commit`, whatever the squash title says. */ +export async function pullRequestsWithCommit(root: string, commit: string): Promise { + const url = `https://api.github.com/repos/${await gitHubRepository(root)}/commits/${commit}/pulls` + const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN + const response = await fetch(url, { + headers: { + Accept: 'application/vnd.github+json', + ...(token ? { Authorization: `Bearer ${token}` } : {}) + }, + signal: AbortSignal.timeout(PULL_REQUEST_LOOKUP_TIMEOUT_MS) + }).catch((error: unknown) => { + // Node's fetch hides the network reason (DNS, TLS, reset) in `cause`. + const cause = error instanceof Error && error.cause ? ` (${String(error.cause)})` : '' + throw new Error( + `Could not ask GitHub which pull requests hold ${commit} (${url}): ${String(error)}${cause}` + ) + }) + const body = await response.text() + // GitHub's answer for a commit it has never seen, which no pull request can hold. + if (response.status === 422 && body.includes('No commit found')) { + return [] + } + if (!response.ok) { + const rateLimited = !token && (response.status === 403 || response.status === 429) + throw new Error( + `GitHub answered ${response.status} to ${url}: ${body.slice(0, 300)}` + + (rateLimited + ? '\nUnauthenticated GitHub API calls are rate-limited; set GITHUB_TOKEN or GH_TOKEN.' + : '') + ) + } + const pulls: unknown = JSON.parse(body) + if (!Array.isArray(pulls)) { + throw new Error( + `GitHub answered ${url} with something other than a list: ${body.slice(0, 300)}` + ) + } + return pulls.flatMap((pull) => (typeof pull?.number === 'number' ? [pull.number] : [])) +} + +/** Fetches a pull request's head into a ref of our own, so no other fetch can move it under us. */ +async function fetchPullRequestHead(root: string, pullRequest: number): Promise { + const local = `refs/rpc-recording-pin/pull/${pullRequest}` + const fetched = await git( + root, + ['fetch', '--quiet', '--no-tags', PIN_REMOTE, `+refs/pull/${pullRequest}/head:${local}`], + PIN_FETCH_TIMEOUT_MS + ) + if (fetched.code !== 0) { + const detail = failureDetail(fetched, PIN_FETCH_TIMEOUT_MS) + throw new Error( + `Could not fetch refs/pull/${pullRequest}/head from ${PIN_REMOTE}, the head of a pull ` + + `request GitHub associates with the recording pin: ${detail}` + ) + } + return local +} + +export async function checkPinReachable( + root: string, + baseline: string, + ref: string, + pullRequestsHolding: PullRequestLookup = pullRequestsWithCommit +): Promise { + const shallow = await git(root, ['rev-parse', '--is-shallow-repository']) + if (shallow.code !== 0) { + throw new Error(`Could not ask git whether the clone is shallow: ${shallow.stderr.trim()}`) + } + if (shallow.stdout.trim() !== 'false') { + return { ok: false, baseline, ref, failure: 'shallow', message: SHALLOW_MESSAGE } + } + const head = await git(root, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]) + if (head.code !== 0) { + throw new Error(`Cannot resolve ${ref} to a commit in this repository`) + } + // Resolved, because the instruction below is a command to paste: `HEAD` in it moves with whatever + // the reader has checked out by the time they read the log. + const resolved = head.stdout.trim() + const present = async () => + (await git(root, ['rev-parse', '--verify', '--quiet', `${baseline}^{commit}`])).code === 0 + if ((await present()) && (await isAncestor(root, baseline, ref))) { + return { ok: true, baseline, ref, pullRequest: null } + } + // A squash drops the branch commit the pin names, and the branch may already be deleted, but + // GitHub keeps the pull request's head ref for good. + const pullRequests = await pullRequestsHolding(root, baseline) + for (const pullRequest of pullRequests) { + const pullRequestHead = await fetchPullRequestHead(root, pullRequest) + if ((await present()) && (await isAncestor(root, baseline, pullRequestHead))) { + return { ok: true, baseline, ref, pullRequest } + } + } + const pullRequestCause = + pullRequests.length === 0 + ? 'and GitHub associates no pull request with it' + : 'and no head of a pull request GitHub associates with it holds it ' + + `(${pullRequests.map((pullRequest) => `refs/pull/${pullRequest}/head`).join(', ')})` + if (!(await present())) { + return { + ok: false, + baseline, + ref, + failure: 'unreachable', + message: repinInstruction( + baseline, + resolved, + `is not a commit in this clone, ${pullRequestCause}` + ) + } + } + return { + ok: false, + baseline, + ref, + failure: 'not-an-ancestor', + message: repinInstruction( + baseline, + resolved, + `is not an ancestor of this commit, ${pullRequestCause}` + ) + } +} diff --git a/mobile/scripts/rpc-recording.mts b/mobile/scripts/rpc-recording.mts index 7ada953da09..c6034a9246e 100644 --- a/mobile/scripts/rpc-recording.mts +++ b/mobile/scripts/rpc-recording.mts @@ -24,7 +24,15 @@ const baseline = await runProcess({ ], cwd: root }) -if (baseline.code !== 0) { +// A pin a squash left only in its pull request's head is a missing object, not product drift. +if (baseline.code !== 0 && baseline.code !== 1) { + throw new Error( + `Could not diff against the pinned baseline ${input.baseline}: ${baseline.stderr.trim()}\n` + + 'If your clone lacks that commit, fetch it with\n' + + ' pnpm --dir mobile exec tsx scripts/rpc-recording-pin-guard.mts reachable' + ) +} +if (baseline.code === 1) { throw new Error('Product sources or lockfile differ from the pinned main baseline') } // Why a second check: `git diff` only sees tracked paths, so an untracked module under the diff --git a/mobile/src/app-update/app-store-update-source.ts b/mobile/src/app-update/app-store-update-source.ts new file mode 100644 index 00000000000..cad70e0840d --- /dev/null +++ b/mobile/src/app-update/app-store-update-source.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' +import type { AppUpdateCheckResult, AppUpdateSource } from './app-update-source' +import { isNewerReleaseVersion } from './app-update-source' + +const LOOKUP_URL = 'https://itunes.apple.com/lookup?bundleId=com.stably.orca.mobile' + +type Fetch = typeof fetch + +const lookupSchema = z.looseObject({ results: z.array(z.unknown()) }) +const listingSchema = z.looseObject({ version: z.string(), trackViewUrl: z.string() }) + +/** + * The store listing's version and page. TestFlight installs have no receipt signal without a new + * dependency; they stay quiet because their build is at or ahead of the store's version. + */ +export function parseAppStoreLookup(reply: unknown): { version: string; url: string } | null { + const lookup = lookupSchema.safeParse(reply) + const listing = listingSchema.safeParse(lookup.success ? lookup.data.results[0] : null) + return listing.success ? { version: listing.data.version, url: listing.data.trackViewUrl } : null +} + +export function createAppStoreUpdateSource(fetchImpl: Fetch): AppUpdateSource { + return { + async check(installedVersion, signal): Promise { + const reply = await fetchImpl(LOOKUP_URL, { signal }) + if (!reply.ok) { + throw new Error(`App Store lookup HTTP ${reply.status}`) + } + const listing = parseAppStoreLookup(await reply.json()) + return listing && isNewerReleaseVersion(listing.version, installedVersion) + ? { kind: 'available', version: listing.version, url: listing.url } + : { kind: 'current' } + } + } +} + +export const appStoreUpdateSource = createAppStoreUpdateSource((input, init) => fetch(input, init)) diff --git a/mobile/src/app-update/app-update-checker.test.ts b/mobile/src/app-update/app-update-checker.test.ts new file mode 100644 index 00000000000..c76c7d1be76 --- /dev/null +++ b/mobile/src/app-update/app-update-checker.test.ts @@ -0,0 +1,293 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { AppUpdatePreferences } from '../storage/app-update-preferences' +import { + APP_UPDATE_CHECK_INTERVAL_MS, + createAppUpdateChecker, + undismissedAppUpdate, + type AppUpdateCheckerDeps +} from './app-update-checker' +import type { AppUpdateCheckResult, AppUpdateSource } from './app-update-source' + +const HOUR = 60 * 60 * 1000 +const T0 = Date.UTC(2026, 8, 28, 12) +const RELEASE_051 = { version: '0.0.51', url: 'https://example.test/0.0.51' } + +type Harness = ReturnType + +function harness(opts: { + stored?: Partial + installedVersion?: string + replies?: (AppUpdateCheckResult | Error | 'hang' | Promise)[] + load?: Promise +}) { + const replies = [...(opts.replies ?? [])] + let foreground: () => void = () => {} + const saves: unknown[] = [] + const source: AppUpdateSource = { + check: vi.fn(async () => { + const reply = replies.shift() ?? { kind: 'current' } + if (reply instanceof Promise) { + return reply + } + if (reply === 'hang') { + return new Promise(() => {}) + } + if (reply instanceof Error) { + throw reply + } + return reply + }) + } + const deps = { + source, + installedVersion: opts.installedVersion ?? '0.0.48', + now: () => Date.now(), + setTimer: (run: () => void, delayMs: number) => setTimeout(run, delayMs), + clearTimer: (handle: ReturnType) => clearTimeout(handle), + subscribeForeground: (listener: () => void) => { + foreground = listener + return () => { + foreground = () => {} + } + }, + loadPreferences: vi.fn(async () => { + await opts.load + return { + lastCheckedAt: null, + latest: null, + dismissedVersion: null, + ...opts.stored + } + }), + saveCheck: async (checkedAt: number, latest: AppUpdatePreferences['latest']) => { + saves.push({ checkedAt, latest }) + }, + saveDismissedVersion: async (version: string) => { + saves.push({ dismissed: version }) + } + } satisfies AppUpdateCheckerDeps + const checker = createAppUpdateChecker(deps) + return { checker, source, saves, deps, foreground: () => foreground() } +} + +const checks = (h: Harness) => vi.mocked(h.source.check).mock.calls.length + +describe('app update checker', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(T0) + }) + afterEach(() => { + vi.useRealTimers() + }) + + it('checks on cold start when it has never succeeded, and records the result', async () => { + const h = harness({ replies: [{ kind: 'available', ...RELEASE_051 }] }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + expect(checks(h)).toBe(1) + expect(h.checker.getSnapshot()).toMatchObject({ lastCheckedAt: T0, available: RELEASE_051 }) + expect(h.saves).toEqual([{ checkedAt: T0, latest: RELEASE_051 }]) + }) + + it('skips the cold-start check inside 24 h, then runs it when the day is up', async () => { + const h = harness({ stored: { lastCheckedAt: T0 - 2 * HOUR, latest: RELEASE_051 } }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + expect(checks(h)).toBe(0) + expect(h.checker.getSnapshot().available).toEqual(RELEASE_051) + await vi.advanceTimersByTimeAsync(22 * HOUR - 1) + expect(checks(h)).toBe(0) + await vi.advanceTimersByTimeAsync(1) + expect(checks(h)).toBe(1) + }) + + it('checks on foreground only once the last success is 24 h old', async () => { + const h = harness({ stored: { lastCheckedAt: T0 - 23 * HOUR } }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + h.foreground() + await vi.advanceTimersByTimeAsync(0) + expect(checks(h)).toBe(0) + // Background suspends timers; the system clock still moves. + vi.setSystemTime(T0 + HOUR) + h.foreground() + await vi.advanceTimersByTimeAsync(0) + expect(checks(h)).toBe(1) + }) + + it('retries a failed check after 1 h, not sooner, and keeps the last success time', async () => { + const h = harness({ + stored: { lastCheckedAt: T0 - 30 * HOUR }, + replies: [new Error('offline')] + }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + expect(checks(h)).toBe(1) + expect(h.checker.getSnapshot().lastCheckedAt).toBe(T0 - 30 * HOUR) + h.foreground() + await vi.advanceTimersByTimeAsync(HOUR - 1) + expect(checks(h)).toBe(1) + await vi.advanceTimersByTimeAsync(1) + expect(checks(h)).toBe(2) + expect(h.checker.getSnapshot().lastCheckedAt).toBe(T0 + HOUR) + }) + + it('bounds a check at 8 s and counts it as failed', async () => { + const h = harness({ stored: { lastCheckedAt: T0 }, replies: ['hang'] }) + h.checker.start() + const outcome = h.checker.checkNow() + await vi.advanceTimersByTimeAsync(8000) + await expect(outcome).resolves.toBe('failed') + expect(h.checker.getSnapshot().checking).toBe(false) + }) + + it('joins a manual check to one already in flight', async () => { + const h = harness({ stored: { lastCheckedAt: T0 }, replies: [{ kind: 'current' }] }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + const [first, second] = [h.checker.checkNow(), h.checker.checkNow()] + await expect(Promise.all([first, second])).resolves.toEqual(['up-to-date', 'up-to-date']) + expect(checks(h)).toBe(1) + }) + + it('manual check reschedules the next automatic check a full day out', async () => { + const h = harness({ stored: { lastCheckedAt: T0 - 23 * HOUR } }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + await h.checker.checkNow() + await vi.advanceTimersByTimeAsync(24 * HOUR - 1) + expect(checks(h)).toBe(1) + await vi.advanceTimersByTimeAsync(1) + expect(checks(h)).toBe(2) + }) + + it('dismisses one version only; the next release offers again', async () => { + const h = harness({ + stored: { lastCheckedAt: T0 - 25 * HOUR }, + replies: [ + { kind: 'available', ...RELEASE_051 }, + { kind: 'available', version: '0.0.52', url: 'https://example.test/0.0.52' } + ] + }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + h.checker.dismiss('0.0.51') + await vi.advanceTimersByTimeAsync(0) + expect(undismissedAppUpdate(h.checker.getSnapshot())).toBeNull() + expect(h.checker.getSnapshot().available).toEqual(RELEASE_051) + expect(h.saves).toContainEqual({ dismissed: '0.0.51' }) + await h.checker.checkNow() + expect(undismissedAppUpdate(h.checker.getSnapshot())?.version).toBe('0.0.52') + }) + + it('keeps a stored dismissal across restarts', async () => { + const h = harness({ + stored: { lastCheckedAt: T0, latest: RELEASE_051, dismissedVersion: '0.0.51' } + }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + expect(undismissedAppUpdate(h.checker.getSnapshot())).toBeNull() + }) + + it('stops offering a stored update once the binary has been updated to it', async () => { + const h = harness({ + installedVersion: '0.0.51', + stored: { lastCheckedAt: T0, latest: RELEASE_051 } + }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + expect(h.checker.getSnapshot().available).toBeNull() + }) + + it('stops scheduling after stop()', async () => { + const h = harness({ stored: { lastCheckedAt: T0 - 2 * HOUR } }) + const stop = h.checker.start() + await vi.advanceTimersByTimeAsync(0) + stop() + await vi.advanceTimersByTimeAsync(2 * APP_UPDATE_CHECK_INTERVAL_MS) + expect(checks(h)).toBe(0) + }) + + it('keeps the 1 h retry for a manual check that fails while the store is still loading', async () => { + let finishLoad: () => void = () => {} + const h = harness({ + stored: { lastCheckedAt: T0 - 30 * HOUR }, + replies: [new Error('offline')], + load: new Promise((resolve) => { + finishLoad = resolve + }) + }) + h.checker.start() + const outcome = h.checker.checkNow() + finishLoad() + await expect(outcome).resolves.toBe('failed') + await vi.advanceTimersByTimeAsync(HOUR - 1) + expect(checks(h)).toBe(1) + await vi.advanceTimersByTimeAsync(1) + expect(checks(h)).toBe(2) + }) + + it('applies one load and runs one check across a StrictMode-style double start', async () => { + const h = harness({}) + h.checker.start()() + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + expect(h.deps.loadPreferences).toHaveBeenCalledTimes(1) + expect(checks(h)).toBe(1) + }) + + function deferredReply() { + let answer: (result: AppUpdateCheckResult) => void = () => {} + const reply = new Promise((resolve) => { + answer = resolve + }) + return { reply, answer: (result: AppUpdateCheckResult) => answer(result) } + } + + it('arms no timer for a check that finishes after stop()', async () => { + const pending = deferredReply() + const h = harness({ stored: { lastCheckedAt: T0 }, replies: [pending.reply] }) + const stop = h.checker.start() + await vi.advanceTimersByTimeAsync(0) + const outcome = h.checker.checkNow() + await vi.advanceTimersByTimeAsync(0) + stop() + pending.answer({ kind: 'current' }) + await outcome + expect(vi.getTimerCount()).toBe(0) + }) + + it('schedules the next check when a check in flight across stop and restart completes', async () => { + const pending = deferredReply() + const h = harness({ stored: { lastCheckedAt: T0 }, replies: [pending.reply] }) + const stop = h.checker.start() + await vi.advanceTimersByTimeAsync(0) + const outcome = h.checker.checkNow() + await vi.advanceTimersByTimeAsync(0) + stop() + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + pending.answer({ kind: 'current' }) + await outcome + expect(vi.getTimerCount()).toBe(1) + await vi.advanceTimersByTimeAsync(24 * HOUR) + expect(checks(h)).toBe(2) + }) + + it('runs the check when the armed timer fires even if the clock stepped back', async () => { + const h = harness({ stored: { lastCheckedAt: T0 - 2 * HOUR } }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + vi.setSystemTime(Date.now() - 10 * 60 * 1000) + await vi.advanceTimersByTimeAsync(22 * HOUR) + expect(checks(h)).toBe(1) + }) + + it('treats a stored check time ahead of the clock as never checked', async () => { + const h = harness({ stored: { lastCheckedAt: T0 + 365 * 24 * HOUR } }) + h.checker.start() + await vi.advanceTimersByTimeAsync(0) + expect(checks(h)).toBe(1) + }) +}) diff --git a/mobile/src/app-update/app-update-checker.ts b/mobile/src/app-update/app-update-checker.ts new file mode 100644 index 00000000000..7c2855d9373 --- /dev/null +++ b/mobile/src/app-update/app-update-checker.ts @@ -0,0 +1,202 @@ +import type { AppUpdatePreferences, KnownAppUpdate } from '../storage/app-update-preferences' +import { EMPTY_APP_UPDATE_PREFERENCES } from '../storage/app-update-preferences' +import type { AppUpdateSource } from './app-update-source' +import { isNewerReleaseVersion } from './app-update-source' + +// Same cadence as the desktop updater (src/main/updater-events.ts). +export const APP_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 +const APP_UPDATE_RETRY_INTERVAL_MS = 60 * 60 * 1000 +const APP_UPDATE_CHECK_TIMEOUT_MS = 8000 + +type TimerHandle = ReturnType + +export type AppUpdateCheckOutcome = 'available' | 'up-to-date' | 'failed' + +export type AppUpdateState = { + readonly lastCheckedAt: number | null + /** Newer than the installed binary; recomputed from `latest`, so installing it clears this. */ + readonly available: KnownAppUpdate | null + readonly dismissedVersion: string | null + readonly checking: boolean +} + +/** The update home should offer: dismissal hides only the version that was dismissed. */ +export function undismissedAppUpdate(state: AppUpdateState): KnownAppUpdate | null { + return state.available && state.available.version !== state.dismissedVersion + ? state.available + : null +} + +export type AppUpdateCheckerDeps = { + source: AppUpdateSource | null + installedVersion: string | null + now: () => number + setTimer: (run: () => void, delayMs: number) => TimerHandle + clearTimer: (handle: TimerHandle) => void + subscribeForeground: (onForeground: () => void) => () => void + loadPreferences: () => Promise + saveCheck: (checkedAt: number, latest: KnownAppUpdate | null) => Promise + saveDismissedVersion: (version: string) => Promise +} + +export function createAppUpdateChecker(deps: AppUpdateCheckerDeps) { + let prefs = EMPTY_APP_UPDATE_PREFERENCES + let loaded: Promise | null = null + let inFlight: Promise | null = null + let nextDueAt = 0 + let timer: TimerHandle | null = null + let activeStarts = 0 + let snapshot = buildSnapshot() + const listeners = new Set<() => void>() + + function buildSnapshot(): AppUpdateState { + const latest = prefs.latest + const available = + latest && + deps.installedVersion && + isNewerReleaseVersion(latest.version, deps.installedVersion) + ? latest + : null + return { + lastCheckedAt: prefs.lastCheckedAt, + available, + dismissedVersion: prefs.dismissedVersion, + checking: inFlight !== null + } + } + + function publish(): void { + snapshot = buildSnapshot() + for (const listener of listeners) { + listener() + } + } + + function clearScheduled(): void { + if (timer !== null) { + deps.clearTimer(timer) + timer = null + } + } + + function schedule(dueAt: number): void { + nextDueAt = dueAt + clearScheduled() + if (activeStarts > 0) { + timer = deps.setTimer( + () => { + timer = null + // The timer is the due time; re-reading the wall clock would stall on a clock step back. + if (activeStarts > 0 && inFlight === null) { + void checkNow() + } + }, + Math.max(0, dueAt - deps.now()) + ) + } + } + + /** The stored state is read once; every check and dismissal waits for it, so none is overwritten. */ + function ensureLoaded(): Promise { + loaded ??= deps.loadPreferences().then((stored) => { + prefs = stored + // A stored check time ahead of the clock (clock since corrected) would defer the next check. + const checkedAt = stored.lastCheckedAt + nextDueAt = + checkedAt === null || checkedAt > deps.now() ? 0 : checkedAt + APP_UPDATE_CHECK_INTERVAL_MS + publish() + }) + return loaded + } + + function runIfDue(): void { + void ensureLoaded().then(() => { + if (activeStarts > 0 && inFlight === null && deps.now() >= nextDueAt) { + void checkNow() + } + }) + } + + async function runCheck(source: AppUpdateSource, installed: string) { + await ensureLoaded() + const controller = new AbortController() + // Why race: the bound holds even for a request that does not honour the signal. + const timedOut = new Promise((_, reject) => { + controller.signal.addEventListener('abort', () => reject(new Error('update check timed out'))) + }) + const timeout = deps.setTimer(() => controller.abort(), APP_UPDATE_CHECK_TIMEOUT_MS) + try { + const result = await Promise.race([source.check(installed, controller.signal), timedOut]) + const checkedAt = deps.now() + const latest = + result.kind === 'available' ? { version: result.version, url: result.url } : null + prefs = { ...prefs, lastCheckedAt: checkedAt, latest } + schedule(checkedAt + APP_UPDATE_CHECK_INTERVAL_MS) + await deps.saveCheck(checkedAt, latest).catch(() => {}) + return latest ? 'available' : 'up-to-date' + } catch { + schedule(deps.now() + APP_UPDATE_RETRY_INTERVAL_MS) + return 'failed' + } finally { + deps.clearTimer(timeout) + } + } + + /** Runs now whatever the cadence says; joins a check already in flight. */ + function checkNow(): Promise { + const { source, installedVersion } = deps + if (!source || !installedVersion) { + return Promise.resolve('failed') + } + if (inFlight === null) { + inFlight = runCheck(source, installedVersion).finally(() => { + inFlight = null + publish() + }) + publish() + } + return inFlight + } + + /** Cold start: reads what the last run saw, then checks only if the cadence says it is due. */ + function start(): () => void { + let active = true + activeStarts += 1 + const unsubscribe = deps.subscribeForeground(runIfDue) + void ensureLoaded().then(() => { + if (active && inFlight === null) { + schedule(nextDueAt) + } + }) + return () => { + if (!active) { + return + } + active = false + activeStarts -= 1 + unsubscribe() + if (activeStarts === 0) { + clearScheduled() + } + } + } + + function dismiss(version: string): void { + void ensureLoaded().then(() => { + prefs = { ...prefs, dismissedVersion: version } + publish() + void deps.saveDismissedVersion(version).catch(() => {}) + }) + } + + return { + start, + checkNow, + dismiss, + getSnapshot: (): AppUpdateState => snapshot, + subscribe(listener: () => void): () => void { + listeners.add(listener) + return () => listeners.delete(listener) + } + } +} diff --git a/mobile/src/app-update/app-update-runtime.ts b/mobile/src/app-update/app-update-runtime.ts new file mode 100644 index 00000000000..aa2d72c7608 --- /dev/null +++ b/mobile/src/app-update/app-update-runtime.ts @@ -0,0 +1,59 @@ +import Constants from 'expo-constants' +import * as Linking from 'expo-linking' +import { useSyncExternalStore } from 'react' +import { AppState, Platform } from 'react-native' +import { + loadAppUpdatePreferences, + saveAppUpdateCheck, + saveDismissedAppUpdateVersion +} from '../storage/app-update-preferences' +import { appStoreUpdateSource } from './app-store-update-source' +import { createAppUpdateChecker, type AppUpdateState } from './app-update-checker' +import type { AppUpdateSource } from './app-update-source' +import { githubReleaseUpdateSource } from './github-release-update-source' + +/** + * Invariant: this app does not use expo-updates, so `expoConfig` is the manifest embedded in the + * binary and its version is the binary's (app.json `version`, Android `versionName`). SDK 55 + * removed `Constants.nativeAppVersion`; adopting expo-updates would make this the update's version. + */ +export const installedAppVersion: string | null = Constants.expoConfig?.version ?? null + +/** The channel that installed this binary. */ +function resolveAppUpdateSource(): AppUpdateSource | null { + if (Platform.OS === 'android') { + return githubReleaseUpdateSource + } + if (Platform.OS === 'ios') { + return appStoreUpdateSource + } + return null +} + +export const appUpdateChecker = createAppUpdateChecker({ + source: resolveAppUpdateSource(), + installedVersion: installedAppVersion, + now: () => Date.now(), + setTimer: (run, delayMs) => setTimeout(run, delayMs), + clearTimer: (handle) => clearTimeout(handle), + subscribeForeground: (onForeground) => { + const subscription = AppState.addEventListener('change', (state) => { + if (state === 'active') { + onForeground() + } + }) + return () => subscription.remove() + }, + loadPreferences: loadAppUpdatePreferences, + saveCheck: saveAppUpdateCheck, + saveDismissedVersion: saveDismissedAppUpdateVersion +}) + +export function useAppUpdateState(): AppUpdateState { + return useSyncExternalStore(appUpdateChecker.subscribe, appUpdateChecker.getSnapshot) +} + +/** Opens the channel's page for the update: the GitHub release (Android) or App Store (iOS). */ +export function openAppUpdate(url: string): void { + void Linking.openURL(url).catch(() => {}) +} diff --git a/mobile/src/app-update/app-update-source.ts b/mobile/src/app-update/app-update-source.ts new file mode 100644 index 00000000000..3fe41b7a1ab --- /dev/null +++ b/mobile/src/app-update/app-update-source.ts @@ -0,0 +1,24 @@ +import { + compareAppVersions, + isPrereleaseAppVersion, + isValidAppVersion +} from '../../../src/shared/app-version' + +export type AppUpdateCheckResult = + | { kind: 'available'; version: string; url: string } + | { kind: 'current' } + +/** One install channel. Rejects when the channel could not be asked; never retries itself. */ +export type AppUpdateSource = { + check(installedVersion: string, signal: AbortSignal): Promise +} + +/** True for a plain release version newer than the installed one; prerelease suffixes never qualify. */ +export function isNewerReleaseVersion(candidate: string, installed: string): boolean { + return ( + isValidAppVersion(candidate) && + isValidAppVersion(installed) && + !isPrereleaseAppVersion(candidate) && + compareAppVersions(candidate, installed) > 0 + ) +} diff --git a/mobile/src/app-update/app-update-sources.test.ts b/mobile/src/app-update/app-update-sources.test.ts new file mode 100644 index 00000000000..22c04b641a7 --- /dev/null +++ b/mobile/src/app-update/app-update-sources.test.ts @@ -0,0 +1,196 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { createAppStoreUpdateSource, parseAppStoreLookup } from './app-store-update-source' +import { isNewerReleaseVersion } from './app-update-source' +import { + createGithubReleaseUpdateSource, + installableReleaseUrl, + parseMobileAndroidTagVersions +} from './github-release-update-source' + +// Recorded once from the live endpoints on 2026-09-28, unedited. +function fixture(name: string): unknown { + return JSON.parse(readFileSync(new URL(`./fixtures/${name}`, import.meta.url), 'utf8')) +} +const tagRefs = fixture('github-mobile-android-tag-refs.json') +const release050 = fixture('github-release-mobile-android-v0.0.50.json') +const lookup = fixture('itunes-lookup-com.stably.orca.mobile.json') + +const REFS_URL = + 'https://api.github.com/repos/stablyai/orca/git/matching-refs/tags/mobile-android-v' +const releaseUrl = (version: string) => + `https://api.github.com/repos/stablyai/orca/releases/tags/mobile-android-v${version}` + +function fakeFetch(routes: Record) { + const requested: string[] = [] + const fetchImpl: typeof fetch = async (input) => { + const url = String(input) + requested.push(url) + const route = routes[url] + if (!route) { + throw new Error(`unexpected request ${url}`) + } + return new Response(JSON.stringify(route.body ?? {}), { status: route.status }) + } + return { fetchImpl, requested } +} + +const signal = new AbortController().signal + +describe('version compare', () => { + it('orders plain versions numerically and ignores prerelease or malformed candidates', () => { + expect(isNewerReleaseVersion('0.0.51', '0.0.48')).toBe(true) + expect(isNewerReleaseVersion('0.0.10', '0.0.9')).toBe(true) + expect(isNewerReleaseVersion('0.0.48', '0.0.48')).toBe(false) + expect(isNewerReleaseVersion('0.0.47', '0.0.48')).toBe(false) + expect(isNewerReleaseVersion('0.0.52-rc.1', '0.0.48')).toBe(false) + expect(isNewerReleaseVersion('latest', '0.0.48')).toBe(false) + }) +}) + +describe('GitHub release source (Android sideload)', () => { + it('reads every mobile-android tag from the recorded refs', () => { + const versions = parseMobileAndroidTagVersions(tagRefs) + expect(versions).toHaveLength(24) + expect(versions).toContain('0.0.50') + expect(versions).not.toContain('0.0.49') + }) + + it('accepts the recorded release only because it is published with an APK', () => { + expect(installableReleaseUrl(release050)).toBe( + 'https://github.com/stablyai/orca/releases/tag/mobile-android-v0.0.50' + ) + expect(installableReleaseUrl({ ...Object(release050), draft: true })).toBeNull() + expect(installableReleaseUrl({ ...Object(release050), assets: [] })).toBeNull() + }) + + it('offers the newest tag above the installed version once its release is proven', async () => { + const { fetchImpl, requested } = fakeFetch({ + [REFS_URL]: { status: 200, body: tagRefs }, + [releaseUrl('0.0.50')]: { status: 200, body: release050 } + }) + await expect( + createGithubReleaseUpdateSource(fetchImpl).check('0.0.47', signal) + ).resolves.toEqual({ + kind: 'available', + version: '0.0.50', + url: 'https://github.com/stablyai/orca/releases/tag/mobile-android-v0.0.50' + }) + expect(requested).toEqual([REFS_URL, releaseUrl('0.0.50')]) + }) + + it('falls back past a tag whose build never published a release', async () => { + const release048 = { ...Object(release050), html_url: 'https://example.test/048' } + const { fetchImpl } = fakeFetch({ + [REFS_URL]: { status: 200, body: tagRefs }, + [releaseUrl('0.0.50')]: { status: 404 }, + [releaseUrl('0.0.48')]: { status: 200, body: release048 } + }) + await expect( + createGithubReleaseUpdateSource(fetchImpl).check('0.0.47', signal) + ).resolves.toEqual({ + kind: 'available', + version: '0.0.48', + url: 'https://example.test/048' + }) + }) + + it('probes at most three candidates and then reports current', async () => { + const { fetchImpl, requested } = fakeFetch({ + [REFS_URL]: { status: 200, body: tagRefs }, + [releaseUrl('0.0.50')]: { status: 404 }, + [releaseUrl('0.0.48')]: { status: 404 }, + [releaseUrl('0.0.47')]: { status: 404 } + }) + await expect( + createGithubReleaseUpdateSource(fetchImpl).check('0.0.14', signal) + ).resolves.toEqual({ + kind: 'current' + }) + expect(requested).toHaveLength(4) + }) + + it('makes no release probe when nothing is newer', async () => { + const { fetchImpl, requested } = fakeFetch({ [REFS_URL]: { status: 200, body: tagRefs } }) + await expect( + createGithubReleaseUpdateSource(fetchImpl).check('0.0.51', signal) + ).resolves.toEqual({ + kind: 'current' + }) + expect(requested).toEqual([REFS_URL]) + }) + + it('opens the release page GitHub reports, not one built from the tag', async () => { + const renamed = { + ...Object(release050), + html_url: 'https://github.com/stablyai/orca/releases/tag/renamed-page' + } + const { fetchImpl } = fakeFetch({ + [REFS_URL]: { status: 200, body: tagRefs }, + [releaseUrl('0.0.50')]: { status: 200, body: renamed } + }) + await expect( + createGithubReleaseUpdateSource(fetchImpl).check('0.0.48', signal) + ).resolves.toEqual({ + kind: 'available', + version: '0.0.50', + url: 'https://github.com/stablyai/orca/releases/tag/renamed-page' + }) + }) + + it('rejects on a release probe 5xx instead of moving to an older candidate', async () => { + const { fetchImpl, requested } = fakeFetch({ + [REFS_URL]: { status: 200, body: tagRefs }, + [releaseUrl('0.0.50')]: { status: 502 } + }) + await expect( + createGithubReleaseUpdateSource(fetchImpl).check('0.0.47', signal) + ).rejects.toThrow('release HTTP 502') + expect(requested).toEqual([REFS_URL, releaseUrl('0.0.50')]) + }) + + it('rejects when a release probe cannot reach GitHub', async () => { + const { fetchImpl } = fakeFetch({ [REFS_URL]: { status: 200, body: tagRefs } }) + await expect( + createGithubReleaseUpdateSource(fetchImpl).check('0.0.47', signal) + ).rejects.toThrow('unexpected request') + }) + + it('rejects when GitHub refuses, so the check counts as failed rather than current', async () => { + const { fetchImpl } = fakeFetch({ [REFS_URL]: { status: 403 } }) + await expect( + createGithubReleaseUpdateSource(fetchImpl).check('0.0.47', signal) + ).rejects.toThrow('tag refs HTTP 403') + }) +}) + +describe('App Store source (iOS)', () => { + const LOOKUP = 'https://itunes.apple.com/lookup?bundleId=com.stably.orca.mobile' + + it('reads the store version and App Store page from the recorded lookup', () => { + expect(parseAppStoreLookup(lookup)).toEqual({ + version: '0.0.51', + url: 'https://apps.apple.com/us/app/orca-ide/id6766130217?uo=4' + }) + }) + + it('offers the store version only when it is newer than the installed one', async () => { + const { fetchImpl } = fakeFetch({ [LOOKUP]: { status: 200, body: lookup } }) + const source = createAppStoreUpdateSource(fetchImpl) + await expect(source.check('0.0.48', signal)).resolves.toEqual({ + kind: 'available', + version: '0.0.51', + url: 'https://apps.apple.com/us/app/orca-ide/id6766130217?uo=4' + }) + await expect(source.check('0.0.51', signal)).resolves.toEqual({ kind: 'current' }) + }) + + it('stays quiet when the lookup has no listing for this bundle id', async () => { + const { fetchImpl } = fakeFetch({ + [LOOKUP]: { status: 200, body: { resultCount: 0, results: [] } } + }) + await expect(createAppStoreUpdateSource(fetchImpl).check('0.0.1', signal)).resolves.toEqual({ + kind: 'current' + }) + }) +}) diff --git a/mobile/src/app-update/fixtures/github-mobile-android-tag-refs.json b/mobile/src/app-update/fixtures/github-mobile-android-tag-refs.json new file mode 100644 index 00000000000..8be30db967b --- /dev/null +++ b/mobile/src/app-update/fixtures/github-mobile-android-tag-refs.json @@ -0,0 +1,242 @@ +[ + { + "ref": "refs/tags/mobile-android-v0.0.14", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjE0", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.14", + "object": { + "sha": "3247256da0e73409104e17ebf1c476a5cb60b3cf", + "type": "tag", + "url": "https://api.github.com/repos/stablyai/orca/git/tags/3247256da0e73409104e17ebf1c476a5cb60b3cf" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.15", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjE1", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.15", + "object": { + "sha": "7b8adaf5f943f1b07668a8e38662690fd8cbf0d0", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/7b8adaf5f943f1b07668a8e38662690fd8cbf0d0" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.16", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjE2", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.16", + "object": { + "sha": "20882d7a25c33a43b50ea19462a0e50f2deb11c4", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/20882d7a25c33a43b50ea19462a0e50f2deb11c4" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.17", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjE3", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.17", + "object": { + "sha": "c50391df84ee4034eca2cd5da49ca9ae4c5c4f7d", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/c50391df84ee4034eca2cd5da49ca9ae4c5c4f7d" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.18", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjE4", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.18", + "object": { + "sha": "9256bd39daaeda0fd86b67f1f50e70850e4f0005", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/9256bd39daaeda0fd86b67f1f50e70850e4f0005" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.19", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjE5", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.19", + "object": { + "sha": "d7322ec1f239db245c557f85370600ca67639137", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/d7322ec1f239db245c557f85370600ca67639137" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.21", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjIx", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.21", + "object": { + "sha": "ad13532ab1f8ea31fc434da4d503ba0c93260fb3", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/ad13532ab1f8ea31fc434da4d503ba0c93260fb3" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.22", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjIy", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.22", + "object": { + "sha": "748bced2e61d47444d6536eecadf8484394e4609", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/748bced2e61d47444d6536eecadf8484394e4609" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.24", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjI0", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.24", + "object": { + "sha": "3c963dd285783609ab0367cfd95a6b45de2b1603", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/3c963dd285783609ab0367cfd95a6b45de2b1603" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.25", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjI1", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.25", + "object": { + "sha": "d1cc78d5c5229478fb9f1675a3d3f506d332b367", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/d1cc78d5c5229478fb9f1675a3d3f506d332b367" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.26", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjI2", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.26", + "object": { + "sha": "cf04845f5b4d787292f041b05f839a8e151fa41b", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/cf04845f5b4d787292f041b05f839a8e151fa41b" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.27", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjI3", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.27", + "object": { + "sha": "20ebe858af628f3e2b5f73838310636dec6fe0ed", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/20ebe858af628f3e2b5f73838310636dec6fe0ed" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.30", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjMw", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.30", + "object": { + "sha": "ef87461bfe0346bcef163db971c990ea3adce495", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/ef87461bfe0346bcef163db971c990ea3adce495" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.31", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjMx", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.31", + "object": { + "sha": "1c275336eddf252a1d1378f9b55a6033278999e8", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/1c275336eddf252a1d1378f9b55a6033278999e8" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.32", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjMy", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.32", + "object": { + "sha": "c445f26541a0ecb465fc28a7e1a7b5ee5624a0dd", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/c445f26541a0ecb465fc28a7e1a7b5ee5624a0dd" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.36", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjM2", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.36", + "object": { + "sha": "4543bb68263a89ab520cea62ca69d7ac78330dd3", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/4543bb68263a89ab520cea62ca69d7ac78330dd3" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.37", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjM3", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.37", + "object": { + "sha": "5a2b329d8d7e96c974aecf4d3e12b707abd7d732", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/5a2b329d8d7e96c974aecf4d3e12b707abd7d732" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.42", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjQy", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.42", + "object": { + "sha": "20aeb0cb990a17f328b673f51717824d2f9c10ea", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/20aeb0cb990a17f328b673f51717824d2f9c10ea" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.43", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjQz", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.43", + "object": { + "sha": "cbca291aa7d7270fe8ed8cac5d08fc0b0910c309", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/cbca291aa7d7270fe8ed8cac5d08fc0b0910c309" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.44", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjQ0", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.44", + "object": { + "sha": "ecfc547ece7aa9483547df26661e90927d1983cc", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/ecfc547ece7aa9483547df26661e90927d1983cc" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.46", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjQ2", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.46", + "object": { + "sha": "47d462743c2b990b95942c75fb2c1c2493d7f478", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/47d462743c2b990b95942c75fb2c1c2493d7f478" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.47", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjQ3", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.47", + "object": { + "sha": "38c61379ffd19d3f573877ecfa1099e3616974dd", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/38c61379ffd19d3f573877ecfa1099e3616974dd" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.48", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjQ4", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.48", + "object": { + "sha": "7e3840a0683f83831502e15f583856fab8977685", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/7e3840a0683f83831502e15f583856fab8977685" + } + }, + { + "ref": "refs/tags/mobile-android-v0.0.50", + "node_id": "REF_kwDORpCz1toAIHJlZnMvdGFncy9tb2JpbGUtYW5kcm9pZC12MC4wLjUw", + "url": "https://api.github.com/repos/stablyai/orca/git/refs/tags/mobile-android-v0.0.50", + "object": { + "sha": "b5ee803d6c45dda9cfd4bb49d58aac47a22ebdc3", + "type": "commit", + "url": "https://api.github.com/repos/stablyai/orca/git/commits/b5ee803d6c45dda9cfd4bb49d58aac47a22ebdc3" + } + } +] diff --git a/mobile/src/app-update/fixtures/github-release-mobile-android-v0.0.50.json b/mobile/src/app-update/fixtures/github-release-mobile-android-v0.0.50.json new file mode 100644 index 00000000000..96976684578 --- /dev/null +++ b/mobile/src/app-update/fixtures/github-release-mobile-android-v0.0.50.json @@ -0,0 +1,92 @@ +{ + "url": "https://api.github.com/repos/stablyai/orca/releases/391254643", + "assets_url": "https://api.github.com/repos/stablyai/orca/releases/391254643/assets", + "upload_url": "https://uploads.github.com/repos/stablyai/orca/releases/391254643/assets{?name,label}", + "html_url": "https://github.com/stablyai/orca/releases/tag/mobile-android-v0.0.50", + "id": 391254643, + "author": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "node_id": "RE_kwDORpCz1s4XUhJz", + "tag_name": "mobile-android-v0.0.50", + "target_commitish": "main", + "name": "Orca Mobile Android mobile-android-v0.0.50", + "draft": false, + "immutable": false, + "prerelease": true, + "created_at": "2026-09-18T02:37:26Z", + "updated_at": "2026-09-18T06:19:48Z", + "published_at": "2026-09-18T06:19:48Z", + "assets": [ + { + "url": "https://api.github.com/repos/stablyai/orca/releases/assets/571968911", + "id": 571968911, + "node_id": "RA_kwDORpCz1s4iF42P", + "name": "app-release.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 133555371, + "digest": "sha256:79b285ca678e0fa00859f5d9348bfdd9d291eff7e1261cd12012c21e2234bad1", + "download_count": 10693, + "created_at": "2026-09-18T06:19:42Z", + "updated_at": "2026-09-18T06:19:46Z", + "browser_download_url": "https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk" + } + ], + "tarball_url": "https://api.github.com/repos/stablyai/orca/tarball/mobile-android-v0.0.50", + "zipball_url": "https://api.github.com/repos/stablyai/orca/zipball/mobile-android-v0.0.50", + "body": "## What's Changed\n* Keep the floating workspace above a working native chat pane by @brennanb2025 in https://github.com/stablyai/orca/pull/18692\n* feat(relay-infra): dynamic NAT ports and alerts for the 2026-09-04 stall signals by @Jinwoo-H in https://github.com/stablyai/orca/pull/18693\n* fix(orca-profiles): tell the renderer when a cloud session is revoked by @Jinwoo-H in https://github.com/stablyai/orca/pull/18694\n* feat(relay): tell the phone when its desktop is signed out by @Jinwoo-H in https://github.com/stablyai/orca/pull/18698\n* perf(orchestration): bound the worker terminal archive in linear time by @nwparker in https://github.com/stablyai/orca/pull/18622\n* fix(startup): apply the HTTP/1.1 compatibility toggle for migrated profiles, and stop parsing the settings file pre-ready by @nwparker in https://github.com/stablyai/orca/pull/18621\n* perf(git): overlap the three independent submodule status reads by @nwparker in https://github.com/stablyai/orca/pull/18623\n* test(renderer): pin the runtime-target selector identity stability that #18685 fixed by @nwparker in https://github.com/stablyai/orca/pull/18625\n* perf(terminal): stop copying every PTY chunk for two startup-error detectors by @nwparker in https://github.com/stablyai/orca/pull/18626\n* perf(terminal): gate the Command Code banner scan before building its scan windows by @nwparker in https://github.com/stablyai/orca/pull/18628\n* perf(editor): build the closing-fence pattern once per fence, not once per line by @nwparker in https://github.com/stablyai/orca/pull/18629\n* perf(right-sidebar): cache the active checks status instead of rebuilding its cache keys per store write by @nwparker in https://github.com/stablyai/orca/pull/18631\n* perf(renderer): stop two store selectors allocating on every write by @nwparker in https://github.com/stablyai/orca/pull/18632\n* perf(source-control): stop reconciling selection on every panel render by @nwparker in https://github.com/stablyai/orca/pull/18637\n* perf(sidebar): stop building host projections the row model throws away by @nwparker in https://github.com/stablyai/orca/pull/18638\n* perf(agent-hooks): find spool newlines with Buffer.indexOf, not a per-byte loop by @nwparker in https://github.com/stablyai/orca/pull/18639\n* perf(claude-usage): reject non-assistant transcript lines before parsing them by @nwparker in https://github.com/stablyai/orca/pull/18640\n* perf(orchestration): project task columns so task reads hit the statement cache by @nwparker in https://github.com/stablyai/orca/pull/18641\n* perf(agent-hooks): stop cloning the whole status roster on every hook event by @nwparker in https://github.com/stablyai/orca/pull/18642\n* perf(sidebar): share one natural-worktree-id scan and drop a redundant row-key join by @nwparker in https://github.com/stablyai/orca/pull/18647\n* perf(terminal): use real event-loop yields between chunked writes by @nwparker in https://github.com/stablyai/orca/pull/18627\n* perf(settings): commit free-text account settings on a debounce, not per keystroke by @nwparker in https://github.com/stablyai/orca/pull/18651\n* feat(relay-infra): cell crash-rate alert and incident dashboard by @Jinwoo-H in https://github.com/stablyai/orca/pull/18717\n* feat(relay): let cells dial Cloud SQL over private IP by @Jinwoo-H in https://github.com/stablyai/orca/pull/18720\n* fix(desktop): never replay a refresh token after a timeout; jitter relay lease renewal by @Jinwoo-H in https://github.com/stablyai/orca/pull/18719\n* perf(ports): serve unchanged macOS listeners from remembered metadata by @nwparker in https://github.com/stablyai/orca/pull/18650\n* refactor(agent-session-journal): move the session journal onto SQLite by @brennanb2025 in https://github.com/stablyai/orca/pull/18652\n* fix(relay-ops): a thrown health fetch is not a health reading; auth probe does not require /ready by @Jinwoo-H in https://github.com/stablyai/orca/pull/18723\n* fix(agents): detect agent CLIs installed outside a version manager by @nwparker in https://github.com/stablyai/orca/pull/18336\n* perf(relay): per-cell inventory locks, delta counters, and a pool statement timeout by @Jinwoo-H in https://github.com/stablyai/orca/pull/18722\n* fix(worktrees): stop a failed worktree scan from being recorded as an authoritative empty listing by @nwparker in https://github.com/stablyai/orca/pull/18456\n* fix(native-chat): clarify active progress by @brennanb2025 in https://github.com/stablyai/orca/pull/18705\n* fix(browser-pane): stop a dead client-hosted guest from killing the workbench by @nwparker in https://github.com/stablyai/orca/pull/18334\n* feat(claude): move structured native chat onto the Claude Agent SDK and enable it on macOS and Linux by @brennanb2025 in https://github.com/stablyai/orca/pull/18560\n* Fix Smart create retaining a task checkout hash with Create more by @nwparker in https://github.com/stablyai/orca/pull/18727\n* infra(relay): drop the unapplied region label from runtime log metrics by @Jinwoo-H in https://github.com/stablyai/orca/pull/18734\n* feat(native-chat): expose split and move-to-pane actions in Chat UI mode by @brennanb2025 in https://github.com/stablyai/orca/pull/18714\n* fix(ui): unmount project selector before Add Project handoff by @Jinwoo-H in https://github.com/stablyai/orca/pull/18730\n* fix: show agent commands once without startup polling by @nwparker in https://github.com/stablyai/orca/pull/18729\n* fix(crash-reporting): make the own-Chromium gate a real choke point, and stop a refusal leaking the root by @nwparker in https://github.com/stablyai/orca/pull/18459\n* chore: update in-app Android APK link to 0.0.47 by @AmethystLiang in https://github.com/stablyai/orca/pull/18745\n* Answer the structured-session support probe without installing the host by @brennanb2025 in https://github.com/stablyai/orca/pull/18695\n* fix(native-chat): render Claude structured chat through the same UI as Codex by @brennanb2025 in https://github.com/stablyai/orca/pull/18743\n* fix(orchestration): fence the dispatch CLI preamble so it stops rendering as headings by @brennanb2025 in https://github.com/stablyai/orca/pull/18718\n* fix(relay-ops): retry a failed MIG inventory read once before calling a cell's power state unknown by @Jinwoo-H in https://github.com/stablyai/orca/pull/18740\n* fix(relay-ops): accept monitor evidence from an ancestor commit with identical monitor code by @Jinwoo-H in https://github.com/stablyai/orca/pull/18754\n* fix(terminal): stop a hidden pane's unmeasured 80x24 from overwriting a live PTY's size on reattach by @Jinwoo-H in https://github.com/stablyai/orca/pull/18706\n* fix: use fallback shell readiness and assert fish startup timing by @nwparker in https://github.com/stablyai/orca/pull/18755\n* fix(relay-ops): retry transient admin-endpoint failures in same-cap verify and rehome jobs by @Jinwoo-H in https://github.com/stablyai/orca/pull/18769\n* fix(native-chat): make document paths and links clickable in chat by @brennanb2025 in https://github.com/stablyai/orca/pull/18712\n* fix(native-chat): tell old mobile builds why a structured chat is missing by @brennanb2025 in https://github.com/stablyai/orca/pull/18756\n* fix(relay-ops): retry freshness-only preflight failures on the first same-cap wave too by @Jinwoo-H in https://github.com/stablyai/orca/pull/18778\n* fix(browser): match loading surfaces to the Orca theme by @Jinwoo-H in https://github.com/stablyai/orca/pull/18738\n* fix(browser): present Electron's own user agent so Cloudflare Turnstile clears by @Jinwoo-H in https://github.com/stablyai/orca/pull/18749\n* fix(relay-ops): align the cloud-data freshness bar with Cloud Monitoring publish lag by @Jinwoo-H in https://github.com/stablyai/orca/pull/18798\n* ci(package): retry apt fetches and docker builds behind the Ubuntu mirror by @nwparker in https://github.com/stablyai/orca/pull/18797\n* docs(relay): correct why the ConPTY teardown asset diverges from the desktop patch by @nwparker in https://github.com/stablyai/orca/pull/18636\n* fix: recover from an alternate shell install, and close the relay duplicate-echo gap by @nwparker in https://github.com/stablyai/orca/pull/18796\n* feat(mobile): port the restructured native-chat turn status and live tool progress by @brennanb2025 in https://github.com/stablyai/orca/pull/18761\n* perf(persistence): stop the session write re-scanning and rebuilding unchanged state by @nwparker in https://github.com/stablyai/orca/pull/18739\n* feat(native-chat): render agent file edits as inline diff cards by @brennanb2025 in https://github.com/stablyai/orca/pull/18765\n* fix(recovery): fail a renderer recovery reload that never loads, instead of leaving a dead window by @nwparker in https://github.com/stablyai/orca/pull/18466\n* fix(ssh): compile node-pty from the host's own Node headers instead of nodejs.org (STA-6674) by @nwparker in https://github.com/stablyai/orca/pull/18774\n* Bump mobile app.json to 0.0.48 by @brennanb2025 in https://github.com/stablyai/orca/pull/18801\n* fix(sidebar): confirm filter reset before revealing active workspace by @nwparker in https://github.com/stablyai/orca/pull/18708\n* perf(terminals): let idle panes share one process-table capture instead of forking their own by @nwparker in https://github.com/stablyai/orca/pull/18742\n* perf(native-chat): stop rebuilding every transcript row on every stream frame by @nwparker in https://github.com/stablyai/orca/pull/18744\n* perf(terminal): skip the partial-escape-tail walk on ESC-free PTY chunks by @nwparker in https://github.com/stablyai/orca/pull/18748\n* perf(renderer): index four projections that rescanned their inputs per keystroke by @nwparker in https://github.com/stablyai/orca/pull/18747\n* Show Claude background task status in Native Chat by @brennanb2025 in https://github.com/stablyai/orca/pull/18757\n* fix(sidebar): stop ⌘⇧↓ worktree navigation jumping to the first row by @nwparker in https://github.com/stablyai/orca/pull/18804\n* perf(persistence): skip rewriting unchanged terminal scrollback snapshots by @nwparker in https://github.com/stablyai/orca/pull/18764\n* fix(agent-session): refuse a pre-commit structured create with an envelope by @brennanb2025 in https://github.com/stablyai/orca/pull/18697\n* perf(relay): bound the symlink directory probes a remote readDir fans out by @nwparker in https://github.com/stablyai/orca/pull/18752\n* perf(terminal): cheap-tier process inspection for anchored local agent panes by @nwparker in https://github.com/stablyai/orca/pull/18780\n* fix(ssh): let the host say whether it armed the shell-ready marker by @nwparker in https://github.com/stablyai/orca/pull/18802\n* fix(native-chat): say when a structured launch fell back to a terminal by @brennanb2025 in https://github.com/stablyai/orca/pull/18762\n* fix(test): stop detached git maintenance racing the divergence fixture teardown by @nwparker in https://github.com/stablyai/orca/pull/18810\n* fix(cloud): let the same-cap roll isolate Asia cells by @Jinwoo-H in https://github.com/stablyai/orca/pull/18811\n* fix(renderer): stop a Node-only process-table module blanking the app at boot by @nwparker in https://github.com/stablyai/orca/pull/18814\n* feat(i18n): localize onboarding flow to Japanese by @tb-soshiro in https://github.com/stablyai/orca/pull/18787\n* fix(cloud): validate protocol-0 same-cap cell plans without rehome trust lines by @Jinwoo-H in https://github.com/stablyai/orca/pull/18818\n* perf(worktree): skip remote probes with no possible result by @nwparker in https://github.com/stablyai/orca/pull/18821\n* fix(native-chat): stop a settling handoff throwing an unhandled rejection at teardown by @nwparker in https://github.com/stablyai/orca/pull/18824\n* fix(claude): make the recovery barrier cover an exit still climbing the close ladder by @nwparker in https://github.com/stablyai/orca/pull/18826\n* Reduce minimum tab width from 88px to 72px by @AmethystLiang in https://github.com/stablyai/orca/pull/18871\n* perf(automations): reuse collation setup when sorting names by @nwparker in https://github.com/stablyai/orca/pull/18823\n* fix(relay-ops): per-region cell latency bar and attributable preflight failures by @Jinwoo-H in https://github.com/stablyai/orca/pull/18877\n* test(e2e): follow current sidebar project and activity actions by @nwparker in https://github.com/stablyai/orca/pull/18878\n* fix(runtime): bound terminal-wait blocked-prompt rules to the live screen bottom by @Jinwoo-H in https://github.com/stablyai/orca/pull/18817\n* test(e2e): fence SSH recovery and release exited Electron pipes by @nwparker in https://github.com/stablyai/orca/pull/18880\n* feat(perf): lint repeated sort setup and schedule regression contracts by @nwparker in https://github.com/stablyai/orca/pull/18822\n* feat(native-chat): label Codex tool rows by what the command actually did by @brennanb2025 in https://github.com/stablyai/orca/pull/18760\n* test: preserve Docker context in isolated VM recipes by @nwparker in https://github.com/stablyai/orca/pull/18884\n* Fix favicon retention across same-origin navigations by @AmethystLiang in https://github.com/stablyai/orca/pull/18879\n* test: isolate native crash restoration and refresh stale fixtures by @nwparker in https://github.com/stablyai/orca/pull/18883\n* test: pin worker visibility fixture command and handle by @nwparker in https://github.com/stablyai/orca/pull/18897\n* feat(cli): report SSH host platforms by @nwparker in https://github.com/stablyai/orca/pull/18896\n* test: canonicalize setup fixture paths before worktree lookup by @nwparker in https://github.com/stablyai/orca/pull/18912\n* perf(worktree): remove redundant creation and terminal startup work by @nwparker in https://github.com/stablyai/orca/pull/18793\n* test: align desktop platform oracles with native behavior by @nwparker in https://github.com/stablyai/orca/pull/18915\n* test: follow sidebar reveal confirmation with a stable workspace target by @nwparker in https://github.com/stablyai/orca/pull/18921\n* feat(native-chat): model Codex MCP and web-search items instead of leaking opcodes by @brennanb2025 in https://github.com/stablyai/orca/pull/18763\n* fix(native-chat): publish structured session status from the host so the sidebar never goes stale by @brennanb2025 in https://github.com/stablyai/orca/pull/18776\n* feat(native-chat): stop monitored tasks individually by @brennanb2025 in https://github.com/stablyai/orca/pull/18807\n* fix(native-chat): tell a pre-SQLite chat how to carry on by @brennanb2025 in https://github.com/stablyai/orca/pull/18808\n* fix(agent-session): never open a sibling terminal on an unproven create by @brennanb2025 in https://github.com/stablyai/orca/pull/18735\n* refactor(agent-session): consolidate wire type imports below lint limit by @nwparker in https://github.com/stablyai/orca/pull/18930\n* test(e2e): stabilize terminal launch and rename menu fixtures by @nwparker in https://github.com/stablyai/orca/pull/18928\n* fix(automation): keep explicit background launches off screen by @nwparker in https://github.com/stablyai/orca/pull/18898\n* feat(automations): restore column sorting on the list by @nwparker in https://github.com/stablyai/orca/pull/18885\n* test: pin terminal Codex home to an explicit managed account by @nwparker in https://github.com/stablyai/orca/pull/18935\n* test: deliver real terminal input and preserve setup reports by @nwparker in https://github.com/stablyai/orca/pull/18939\n* test: isolate skill cloud fixture ports across workers by @nwparker in https://github.com/stablyai/orca/pull/18942\n* test: align Source Control AI generation fixtures by @nwparker in https://github.com/stablyai/orca/pull/18941\n* ci: reduce runner overhead and disposable package compression by @nwparker in https://github.com/stablyai/orca/pull/18948\n* test: keep artifact share fixtures unexpired across calendar dates by @nwparker in https://github.com/stablyai/orca/pull/18955\n* docs(relay): 2026-09 reconnect findings, checklist, roadmap, and Roll 2 plan by @Jinwoo-H in https://github.com/stablyai/orca/pull/18958\n* test: make SSH artifact regression fixtures reliable at narrow widths by @nwparker in https://github.com/stablyai/orca/pull/18947\n* test: order restart fixture readiness around daemon recovery by @nwparker in https://github.com/stablyai/orca/pull/18949\n* fix(orchestration): typed error codes for dispatch and worker-start refusals by @Jinwoo-H in https://github.com/stablyai/orca/pull/18902\n* ci: skip idle Mac allocations and redundant native compiler setup by @nwparker in https://github.com/stablyai/orca/pull/18954\n* test: isolate Source Control generation from shared repository remotes by @nwparker in https://github.com/stablyai/orca/pull/18962\n* fix(relay): abandon dead client accepts, jitter and lengthen the control lease, fail direct probes fast by @Jinwoo-H in https://github.com/stablyai/orca/pull/18959\n* fix(hooks): register the Claude hook script directly on Windows (#18875) by @OrcaWin in https://github.com/stablyai/orca/pull/18905\n* fix(ci): store vetted refs in a reftable so case-twin branches don't fail the fetch by @nwparker in https://github.com/stablyai/orca/pull/18970\n* test: drain project menu focus restoration before teardown by @nwparker in https://github.com/stablyai/orca/pull/18971\n* ci: reduce dependency, checkout, and test deadline overhead by @nwparker in https://github.com/stablyai/orca/pull/18968\n* ci: preserve case-twin release refs through checkout and verify trust by @nwparker in https://github.com/stablyai/orca/pull/18980\n* test: honor background mode in paired client window helpers by @nwparker in https://github.com/stablyai/orca/pull/18978\n* test: bound release checkout fixtures and gate delayed imports by @nwparker in https://github.com/stablyai/orca/pull/18981\n* fix(native-chat): repair a structured chat tab permanently fenced by an inherited publication epoch by @brennanb2025 in https://github.com/stablyai/orca/pull/18906\n* ci: expose existing E2E spec selection for manual dispatch by @nwparker in https://github.com/stablyai/orca/pull/18987\n* test: isolate window mocks from inherited launch flags by @nwparker in https://github.com/stablyai/orca/pull/18989\n* test: match explorer filenames independently of git badges by @nwparker in https://github.com/stablyai/orca/pull/18997\n* test: synchronize large repository recovery with Retry request by @nwparker in https://github.com/stablyai/orca/pull/18999\n* perf(relay): drain fragmented frame buffers in linear time by @nwparker in https://github.com/stablyai/orca/pull/18891\n* perf(repos): avoid quadratic icon source scans by @nwparker in https://github.com/stablyai/orca/pull/18892\n* perf(browser): assemble fragmented tunnel frames once by @nwparker in https://github.com/stablyai/orca/pull/18893\n* perf(editor): reuse Markdown source blocks while positioning review notes by @nwparker in https://github.com/stablyai/orca/pull/18895\n* perf(editor): reuse live Markdown search matches across unrelated renders by @nwparker in https://github.com/stablyai/orca/pull/18903\n* perf: decode fragmented CLI replies without repeated scans by @nwparker in https://github.com/stablyai/orca/pull/18909\n* perf: avoid file-to-file scans when selecting deletion roots by @nwparker in https://github.com/stablyai/orca/pull/18913\n* perf: find mobile Markdown placeholder prefixes in one scan by @nwparker in https://github.com/stablyai/orca/pull/18914\n* perf: avoid repeated Quick Open exclusion string allocations by @nwparker in https://github.com/stablyai/orca/pull/18916\n* perf: skip unrelated shared symlink probes during Git status by @nwparker in https://github.com/stablyai/orca/pull/18918\n* perf: avoid splitting every path during file autocomplete by @nwparker in https://github.com/stablyai/orca/pull/18919\n* perf: avoid rescanning emitted source in analysis guards by @nwparker in https://github.com/stablyai/orca/pull/18920\n* perf(cli): skip feature formatters during help and error startup by @nwparker in https://github.com/stablyai/orca/pull/18923\n* perf(explorer): remove redundant dotfile path filter allocation by @nwparker in https://github.com/stablyai/orca/pull/18929\n* perf(watcher): skip remaining filesystem checks after cancellation by @nwparker in https://github.com/stablyai/orca/pull/18931\n* perf(hooks): use native reverse search for transcript lines by @nwparker in https://github.com/stablyai/orca/pull/18936\n* perf(skills): skip symlink probes beyond discovery depth by @nwparker in https://github.com/stablyai/orca/pull/18937\n* perf(mobile): cancel direct probes when their owner stops by @nwparker in https://github.com/stablyai/orca/pull/18940\n* perf(jira): preserve replacement attachment download singleflight by @nwparker in https://github.com/stablyai/orca/pull/18944\n* perf(speech): reuse the model download idle timer by @nwparker in https://github.com/stablyai/orca/pull/18945\n* perf(ssh): reuse and release relay startup buffers by @nwparker in https://github.com/stablyai/orca/pull/18953\n* perf(ssh): reuse streamed response idle timers by @nwparker in https://github.com/stablyai/orca/pull/18956\n* perf(browser): reuse decoded single-chunk upload buffers by @nwparker in https://github.com/stablyai/orca/pull/18960\n* perf(tabs): index saved tab order during hydration repair by @nwparker in https://github.com/stablyai/orca/pull/18964\n* perf(palette): reuse allowed quality arrays during matching by @nwparker in https://github.com/stablyai/orca/pull/18966\n* perf: remember equivalent session tab source identities by @nwparker in https://github.com/stablyai/orca/pull/18976\n* perf(cli): skip impossible typo distance comparisons by @nwparker in https://github.com/stablyai/orca/pull/18977\n* fix(tests): stabilize divider viewport and pointer-capture event ordering by @nwparker in https://github.com/stablyai/orca/pull/19004\n* fix(mobile): stop host streams after relay subscription cancellation by @nwparker in https://github.com/stablyai/orca/pull/18926\n* fix(tests): provide a window manager for Linux Electron CI by @nwparker in https://github.com/stablyai/orca/pull/19007\n* fix(windows): copy the daemon host exe verbatim instead of renaming it (MDE T1036) by @OrcaWin in https://github.com/stablyai/orca/pull/17865\n* fix(computer-use): run the Windows runtime as one persistent helper by @OrcaWin in https://github.com/stablyai/orca/pull/17858\n* fix(windows): drop EDR-flagged -ExecutionPolicy Bypass from encoded PowerShell by @OrcaWin in https://github.com/stablyai/orca/pull/17880\n* fix(windows): read command lines from the kernel, not each process's PEB by @OrcaWin in https://github.com/stablyai/orca/pull/17886\n* fix(windows): scan ports natively instead of encoded PowerShell by @OrcaWin in https://github.com/stablyai/orca/pull/17861\n* fix(security): apply the Windows path-hardening ACL that never ran by @OrcaWin in https://github.com/stablyai/orca/pull/17884\n* perf: avoid repeated whitespace scans in diagnostic redaction by @nwparker in https://github.com/stablyai/orca/pull/18908\n* Improve notification view by @AmethystLiang in https://github.com/stablyai/orca/pull/18699\n* test(child-process): lower DIRECT_IMPORTER_PIN to the ground two PRs took by @OrcaWin in https://github.com/stablyai/orca/pull/19009\n* fix(windows): resolve npm/pnpm .cmd shims past cmd.exe by @OrcaWin in https://github.com/stablyai/orca/pull/17869\n* test: lower child-process import pin after ACL migration by @nwparker in https://github.com/stablyai/orca/pull/19012\n* Open open new link should not navigate away from current link by @AmethystLiang in https://github.com/stablyai/orca/pull/18873\n* perf(windows): split the process table into two flag sets by @OrcaWin in https://github.com/stablyai/orca/pull/17866\n* fix(release): stop shipping an unsigned elevate.exe on Windows by @OrcaWin in https://github.com/stablyai/orca/pull/18044\n* fix(windows): sign the NSIS uninstaller via SignPath by @OrcaWin in https://github.com/stablyai/orca/pull/17868\n* fix(tests): complete hidden SSH dialog exits during cleanup by @nwparker in https://github.com/stablyai/orca/pull/18993\n* Support updating existing draft releases when regenerating notes by @AmethystLiang in https://github.com/stablyai/orca/pull/19014\n* fix(windows): drop no-op -ExecutionPolicy Bypass from -Command spawns by @OrcaWin in https://github.com/stablyai/orca/pull/17873\n* perf(search): assemble fragmented subprocess lines incrementally by @nwparker in https://github.com/stablyai/orca/pull/18973\n* test: use a valid repo filter in workspace reveal coverage by @nwparker in https://github.com/stablyai/orca/pull/19017\n* test: cover native X11 Hangul-plus-digit PTY bytes in CI by @nwparker in https://github.com/stablyai/orca/pull/19013\n* Stop evicted and expired worktree preparations by @nwparker in https://github.com/stablyai/orca/pull/18951\n* Let worktree creation proceed during stale preparation reclamation by @nwparker in https://github.com/stablyai/orca/pull/18967\n* fix(release): revalidate draft state before patching generated notes by @AmethystLiang in https://github.com/stablyai/orca/pull/19019\n* perf(worktree): overlap base refresh with prepared checkout by @nwparker in https://github.com/stablyai/orca/pull/18998\n* test: await fresh inventory after headless terminal materialization by @nwparker in https://github.com/stablyai/orca/pull/19028\n* fix(sidebar): stop a missed pointerup from hiding a remote host section by @nwparker in https://github.com/stablyai/orca/pull/19032\n* test: wait for board pointer readiness before marquee selection by @nwparker in https://github.com/stablyai/orca/pull/19029\n* fix(build): pin config/relay-assets LF so one release is one relay hash by @OrcaWin in https://github.com/stablyai/orca/pull/19024\n* docs(relay): record Roll 2 phase 0/1 (code merge, image, director deploy) by @Jinwoo-H in https://github.com/stablyai/orca/pull/18979\n* feat(github-projects): render Roadmap project views as a timeline by @NaoyaTatetsu in https://github.com/stablyai/orca/pull/17795\n* fix(mobile): preserve iPad hardware keyboard focus by @ramarivera in https://github.com/stablyai/orca/pull/12772\n* test: await reattach replay before checking mouse reset by @nwparker in https://github.com/stablyai/orca/pull/19039\n* test: require recorded Git activity in polling regression by @nwparker in https://github.com/stablyai/orca/pull/19041\n* fix(native-chat): resume a structured chat from Agent Session History by @OrcaWin in https://github.com/stablyai/orca/pull/18933\n* fix(source-control): stack Retry below the too-many-changes message by @nwparker in https://github.com/stablyai/orca/pull/19037\n* test: preserve Windows golden command failures by @nwparker in https://github.com/stablyai/orca/pull/19047\n* test: canonicalize Windows fresh-profile fixture path by @nwparker in https://github.com/stablyai/orca/pull/19049\n* test: repair Windows paste setup and newline expectations by @nwparker in https://github.com/stablyai/orca/pull/19050\n* fix(source-control): stack the Create PR notice's settings link below its message by @nwparker in https://github.com/stablyai/orca/pull/19046\n* test: align Windows shell icons with project runtime ownership by @nwparker in https://github.com/stablyai/orca/pull/19053\n* test: exercise supported ConPTY keyboard protocol reset by @nwparker in https://github.com/stablyai/orca/pull/19054\n* fix(native-chat): honor structured routing with saved options by @brennanb2025 in https://github.com/stablyai/orca/pull/19040\n* fix(packaging): ship Claude agent SDK with desktop builds by @brennanb2025 in https://github.com/stablyai/orca/pull/19042\n* test: reject unsupported app-server in golden agent fixture by @nwparker in https://github.com/stablyai/orca/pull/19056\n* test: reject unsupported app-server in WSL golden stub by @nwparker in https://github.com/stablyai/orca/pull/19062\n* test: canonicalize native Windows paths during repository teardown by @nwparker in https://github.com/stablyai/orca/pull/19064\n* test: run real WSL terminal launch and paste in PR CI by @nwparker in https://github.com/stablyai/orca/pull/19072\n* test: restore SSH bulk-open freeze coverage in headed CI by @nwparker in https://github.com/stablyai/orca/pull/19081\n* test: enable direct and client-hosted SSH browser coverage by @nwparker in https://github.com/stablyai/orca/pull/19090\n* test: reconnect after replacing same-ID runtime pairing by @nwparker in https://github.com/stablyai/orca/pull/19094\n* test: enable Docker SSH browser network route coverage in CI by @nwparker in https://github.com/stablyai/orca/pull/19095\n* docs(relay): Roll 2 cell roll record and checklist ticks by @Jinwoo-H in https://github.com/stablyai/orca/pull/19096\n* test: repair nested SSH fixture after HUB restart by @nwparker in https://github.com/stablyai/orca/pull/19098\n* test: enable localhost SSH terminal and hook journey in CI by @nwparker in https://github.com/stablyai/orca/pull/19097\n* Bump mobile Android versionCode to 16 to match the 0.0.48 release by @brennanb2025 in https://github.com/stablyai/orca/pull/19101\n* test: reuse authoritative SSH connection readiness in localhost fixture by @nwparker in https://github.com/stablyai/orca/pull/19102\n* test: follow retained Activity sidebar during pane selection by @nwparker in https://github.com/stablyai/orca/pull/19107\n* test: isolate source-control generation repositories per scenario by @nwparker in https://github.com/stablyai/orca/pull/19105\n* test: cover SSH reattach replay and enable deterministic Codex CI by @nwparker in https://github.com/stablyai/orca/pull/19106\n* fix(chat): stop terminal focus recovery from stealing Cmd+C in the Chat UI by @brennanb2025 in https://github.com/stablyai/orca/pull/18751\n* Add persistent turn-scoped chat activity indicator by @brennanb2025 in https://github.com/stablyai/orca/pull/19044\n* feat(orchestration): make multi-agent workflows durable by @Jinwoo-H in https://github.com/stablyai/orca/pull/16904\n* fix(ssh-relay): daemon owns the endpoint credential; a losing start never rotates it by @Jinwoo-H in https://github.com/stablyai/orca/pull/19052\n* Recover branch compare on visibility change by @AmethystLiang in https://github.com/stablyai/orca/pull/19021\n* fix(codex): keep large app-server replies alive by @brennanb2025 in https://github.com/stablyai/orca/pull/18590\n* docs(orchestration): never pick a worker model the user did not name by @Jinwoo-H in https://github.com/stablyai/orca/pull/19109\n* feat(mobile): structured native Claude chat by @brennanb2025 in https://github.com/stablyai/orca/pull/18741\n* Seed Cmd-J filter from sidebar scope by @AmethystLiang in https://github.com/stablyai/orca/pull/19036\n* skills: rewrite and trim the seven non-orchestration guides (do not merge without Jinwoo) by @Jinwoo-H in https://github.com/stablyai/orca/pull/18724\n* fix(ci): stop Android release notes exceeding the GitHub body limit by @brennanb2025 in https://github.com/stablyai/orca/pull/19114\n* Update mobile 0.0.48 Android download links by @brennanb2025 in https://github.com/stablyai/orca/pull/19117\n* fix(orchestration): keep worker lineage across app restart (STA-6366) by @Jinwoo-H in https://github.com/stablyai/orca/pull/19121\n* fix(browser): select full URL on initial address bar click by @Jinwoo-H in https://github.com/stablyai/orca/pull/19118\n* fix(windows): unblock structured native chat by exposing process creation time by @brennanb2025 in https://github.com/stablyai/orca/pull/18986\n* Revert \"skills: rewrite the seven non-orchestration guides to one outcome-first standard (#18724)\" by @Jinwoo-H in https://github.com/stablyai/orca/pull/19126\n* perf(store): keep the repo list's identity through workspace hydration by @nwparker in https://github.com/stablyai/orca/pull/19057\n* perf(tabs): keep tab-model identity when reconciliation changed something else by @nwparker in https://github.com/stablyai/orca/pull/19063\n* perf(mobile): skip the agent-status projection join when nothing changed by @nwparker in https://github.com/stablyai/orca/pull/19115\n* perf(worktrees): stop worktree removal from replacing maps it never touched by @nwparker in https://github.com/stablyai/orca/pull/19058\n* perf(selectors): stop two always-mounted selectors allocating per store write by @nwparker in https://github.com/stablyai/orca/pull/19113\n* perf(terminals): stop closing a tab from replacing maps it never touched by @nwparker in https://github.com/stablyai/orca/pull/19060\n* perf(terminals): keep shutdown maps' identity when there is nothing to clear by @nwparker in https://github.com/stablyai/orca/pull/19112\n* perf(store): detect Zustand rerender churn the current audit cannot see by @nwparker in https://github.com/stablyai/orca/pull/19059\n* fix(native-chat): stop seeding a stray terminal beside a chat create by @brennanb2025 in https://github.com/stablyai/orca/pull/19123\n* Improve cmd j ranking by @AmethystLiang in https://github.com/stablyai/orca/pull/19005\n* feat(native-chat): offer the link-action popover for chat links by @brennanb2025 in https://github.com/stablyai/orca/pull/19130\n* fix(ui): keep source control headers readable in narrow sidebars by @nwparker in https://github.com/stablyai/orca/pull/19146\n* fix(gh): log when gh/glab is killed at its deadline by @nwparker in https://github.com/stablyai/orca/pull/18555\n* fix: restore the full sidebar agent row for structured native chat by @brennanb2025 in https://github.com/stablyai/orca/pull/19137\n* fix(native-chat): suppress provider user echoes in Claude and Codex by @brennanb2025 in https://github.com/stablyai/orca/pull/19136\n* fix(native-chat): settle a structured send the provider proves it received after the ack window by @brennanb2025 in https://github.com/stablyai/orca/pull/19140\n* Show provider activity in chat turn tails by @brennanb2025 in https://github.com/stablyai/orca/pull/19055\n* fix(ui): ignore the persistent workspace list when detecting overlays by @nwparker in https://github.com/stablyai/orca/pull/18881\n* fix(windows): reject stale parent PID links in shutdown snapshots by @brennanb2025 in https://github.com/stablyai/orca/pull/19149\n* fix(native-chat): auto-rename the workspace on a structured chat's first turn by @brennanb2025 in https://github.com/stablyai/orca/pull/19138\n* Fix native chat completion sorting and restored activity timestamps by @brennanb2025 in https://github.com/stablyai/orca/pull/19144\n* test: exercise packaged browser compatibility in scheduled CI by @nwparker in https://github.com/stablyai/orca/pull/19157\n* test: refresh palette identities and structured-session journal fixtures by @nwparker in https://github.com/stablyai/orca/pull/19165\n* fix(terminal): preserve ordinary foreground command names by @nwparker in https://github.com/stablyai/orca/pull/18882\n* fix(cmd-j): pass browser tab ownership into palette search by @nwparker in https://github.com/stablyai/orca/pull/18925\n* fix: keep macOS shell ownership proof within recovery budget by @nwparker in https://github.com/stablyai/orca/pull/18932\n* fix: open editor rename after context menu releases focus by @nwparker in https://github.com/stablyai/orca/pull/18934\n* fix: avoid starting browser helpers just to reset absent sessions by @nwparker in https://github.com/stablyai/orca/pull/18952\n* fix: prefer retained provider snapshots during hidden terminal recovery by @nwparker in https://github.com/stablyai/orca/pull/18972\n* fix: preserve overlay focus during terminal mounting and layout by @nwparker in https://github.com/stablyai/orca/pull/18982\n* fix: preserve terminal command probes across control frames by @nwparker in https://github.com/stablyai/orca/pull/19006\n* fix: preserve paired host sessions during startup residue cleanup by @nwparker in https://github.com/stablyai/orca/pull/18922\n* fix: preserve renderer browser publication during client-hosted page updates by @nwparker in https://github.com/stablyai/orca/pull/18961\n* fix: honor remote terminal insertion in the calling client by @nwparker in https://github.com/stablyai/orca/pull/18995\n* fix: keep paired tab updates live after runtime terminal fallback by @nwparker in https://github.com/stablyai/orca/pull/19022\n* fix: avoid credit deadlock during large SSH PTY recovery by @nwparker in https://github.com/stablyai/orca/pull/19026\n* fix: recognize working WSL1 without a WSL2 kernel by @nwparker in https://github.com/stablyai/orca/pull/19061\n* fix(ports): coalesce advertised URL refresh bursts by @nwparker in https://github.com/stablyai/orca/pull/19150\n* perf(agent-status): stop no-op retirements rebuilding the retired-pane-key map by @nwparker in https://github.com/stablyai/orca/pull/19142\n* perf(worktrees): stop worktree teardown replacing arrays and maps it never touched by @nwparker in https://github.com/stablyai/orca/pull/19145\n* fix(cmd-j): remove duplicate browser ownership inputs by @nwparker in https://github.com/stablyai/orca/pull/19172\n* fix: recognize the kimi-code process as the kimi agent by @Aladex in https://github.com/stablyai/orca/pull/18634\n* fix: release floating terminal WebGL contexts while closed by @nwparker in https://github.com/stablyai/orca/pull/19000\n* fix: fence browser recovery to attach inventory placements by @nwparker in https://github.com/stablyai/orca/pull/18910\n* test: refresh paired palette host-qualified row locators by @nwparker in https://github.com/stablyai/orca/pull/19175\n* test: cover native Wayland Hangul in isolated CI by @nwparker in https://github.com/stablyai/orca/pull/19174\n* test(e2e): scope paired preview link checks to confirmation by @nwparker in https://github.com/stablyai/orca/pull/18924\n* test: await rendered remote agent placement before checking mirrors by @nwparker in https://github.com/stablyai/orca/pull/18983\n* test: scope selected runtime project assertions to the sidebar by @nwparker in https://github.com/stablyai/orca/pull/19003\n* test: confirm running-command prompts when closing tabs by @nwparker in https://github.com/stablyai/orca/pull/18965\n* Fix MiniMax China usage routing and credential handling by @weekbin in https://github.com/stablyai/orca/pull/14929\n* fix(rate-limits): stop reporting Grok usage as 0% when the API omits the percent by @TimothyVang in https://github.com/stablyai/orca/pull/17936\n* test: enable software WebGL for Linux CI headful specs by @nwparker in https://github.com/stablyai/orca/pull/19001\n* test: restore snapshot path before draining hydration FIFO by @nwparker in https://github.com/stablyai/orca/pull/19186\n* test: return paired browser state from the successful poll by @nwparker in https://github.com/stablyai/orca/pull/19189\n* feat: real background push notifications for the mobile app (#8129) by @Jinwoo-H in https://github.com/stablyai/orca/pull/18554\n* test: align worker recovery fixture with ownership policy by @nwparker in https://github.com/stablyai/orca/pull/19190\n* feat(orchestration): orchestrate native-born structured chat sessions by @brennanb2025 in https://github.com/stablyai/orca/pull/18827\n* fix(native-chat): remember structured chat model and effort picks by @brennanb2025 in https://github.com/stablyai/orca/pull/19147\n* fix(native-chat): list the slash commands and skills a structured Claude session actually loaded by @brennanb2025 in https://github.com/stablyai/orca/pull/19127\n* skills: rewrite and trim the seven non-orchestration guides by @Jinwoo-H in https://github.com/stablyai/orca/pull/19128\n* feat(chat): support structured /clear and /compact commands by @brennanb2025 in https://github.com/stablyai/orca/pull/19164\n* revert: hold mobile push feature for user testing by @Jinwoo-H in https://github.com/stablyai/orca/pull/19203\n* fix(editor): support Shift+wheel scrolling in combined diffs by @brohoya in https://github.com/stablyai/orca/pull/11756\n* feat(native-chat): resume an Agent Session History row into a new structured chat by @brennanb2025 in https://github.com/stablyai/orca/pull/19176\n* fix(mobile): stop double-scaling commit timestamps in history rows by @blade035 in https://github.com/stablyai/orca/pull/17731\n* fix(runtime): apply the structured-chat setting to every RPC caller by @brennanb2025 in https://github.com/stablyai/orca/pull/18700\n* fix(native-chat): settle structured chat turns stranded by a restart by @brennanb2025 in https://github.com/stablyai/orca/pull/19122\n* fix: eliminate quadratic whitespace trimming during fullscreen scrolling by @nwparker in https://github.com/stablyai/orca/pull/19214\n* Restore independent push gateway deployment by @Jinwoo-H in https://github.com/stablyai/orca/pull/19225\n* Fix push deployment source archive path by @Jinwoo-H in https://github.com/stablyai/orca/pull/19231\n* Reorganize MiniMax modules and de-duplicate shared test state by @nwparker in https://github.com/stablyai/orca/pull/19197\n* fix: repaint hidden output overflow after answered restore deadline by @nwparker in https://github.com/stablyai/orca/pull/18904\n* fix: retain MSYS shell descendants in their terminal job by @nwparker in https://github.com/stablyai/orca/pull/19068\n* fix(test): unbreak main — admit the adoption-replay create fixture through the structured-chat gate by @nwparker in https://github.com/stablyai/orca/pull/19246\n* Fix MiniMax credential-expiry reporting, region sync, and refresh by @nwparker in https://github.com/stablyai/orca/pull/19250\n* fix(orchestration): recover Codex idle after completion title race by @Jinwoo-H in https://github.com/stablyai/orca/pull/19243\n* feat(relay): time successful client accepts and control round trips by @Jinwoo-H in https://github.com/stablyai/orca/pull/19232\n* fix(relay): rehome hosts to their preferred region in either direction by @Jinwoo-H in https://github.com/stablyai/orca/pull/19241\n* perf(mobile): cut the relay reconnect critical path and admit dead sockets faster by @Jinwoo-H in https://github.com/stablyai/orca/pull/19236\n* feat(mobile): draw the last known tab strip while a session reconnects by @Jinwoo-H in https://github.com/stablyai/orca/pull/19258\n* feat(relay): alert on far-cell placement and skewed region hints by @Jinwoo-H in https://github.com/stablyai/orca/pull/19253\n* fix(test): give federation tests a real read-after-write sync barrier by @Jinwoo-H in https://github.com/stablyai/orca/pull/19262\n* revert(mobile): hold the relay reconnect path and cache-first reconnect for a separate mobile pass by @Jinwoo-H in https://github.com/stablyai/orca/pull/19265\n* relay: give the asia-east2 cells the regional rehome identity by @Jinwoo-H in https://github.com/stablyai/orca/pull/19239\n* fix(relay): emit pending-conn details to hosts that advertise the capability (cell side) by @Jinwoo-H in https://github.com/stablyai/orca/pull/19266\n* fix(relay): bound control RTT samples per ping and per flush window by @Jinwoo-H in https://github.com/stablyai/orca/pull/19268\n* fix(relay-ops): let the rehome trust probe approve the asia-east2 cells by @Jinwoo-H in https://github.com/stablyai/orca/pull/19275\n* fix(codex): distinguish personal and enterprise accounts sharing an email by @nwparker in https://github.com/stablyai/orca/pull/19279\n* test(relay): prove the pending-conn capability header reaches acceptControl by @Jinwoo-H in https://github.com/stablyai/orca/pull/19274\n* feat(windows): enable structured Codex chat on native Windows by @brennanb2025 in https://github.com/stablyai/orca/pull/18519\n* test(ssh): isolate the MFA fixture from the developer's real ~/.ssh by @brennanb2025 in https://github.com/stablyai/orca/pull/19300\n* Add localization for activity view and sidebar by @OrcaWin in https://github.com/stablyai/orca/pull/18589\n* fix(native-chat): make structured chat tabs renameable by @brennanb2025 in https://github.com/stablyai/orca/pull/19153\n* feat(native-chat): show Codex subagent activity instead of opcode rows by @brennanb2025 in https://github.com/stablyai/orca/pull/18773\n* fix(agent-status): stop a stale self-authored agent title from faking a pending question by @brennanb2025 in https://github.com/stablyai/orca/pull/19237\n* fix(terminal): remount a pane left unbound by a spawn that returned no PTY id by @brennanb2025 in https://github.com/stablyai/orca/pull/19223\n* fix(relay): stop rejecting the near region on a cold first probe by @Jinwoo-H in https://github.com/stablyai/orca/pull/19233\n* perf(mobile): cut the relay reconnect critical path and admit dead sockets faster (mobile pass) by @Jinwoo-H in https://github.com/stablyai/orca/pull/19280\n* perf(mobile): open a session with parallel startup RPCs and a pre-warmed terminal engine by @Jinwoo-H in https://github.com/stablyai/orca/pull/19260\n* fix(relay): attach a phone whose accept straddles a control rebind (desktop side) by @Jinwoo-H in https://github.com/stablyai/orca/pull/19238\n* tools: add a phone-vantage relay connect benchmark by @Jinwoo-H in https://github.com/stablyai/orca/pull/19251\n* feat(mobile): draw the last known tab strip while a session reconnects (mobile pass) by @Jinwoo-H in https://github.com/stablyai/orca/pull/19281\n* feat(relay): log the region probe and name the assigned cell by @Jinwoo-H in https://github.com/stablyai/orca/pull/19307\n* perf(mobile): race the direct and relay dials from t=0 on every reconnect by @Jinwoo-H in https://github.com/stablyai/orca/pull/19308\n* feat(mobile): time relay dial stages so diagnostics say where a slow connect went by @Jinwoo-H in https://github.com/stablyai/orca/pull/19245\n* Persist agents sidebar search visibility as pairing-local preference by @AmethystLiang in https://github.com/stablyai/orca/pull/19313\n* feat(chat): add structured session rewind backend by @brennanb2025 in https://github.com/stablyai/orca/pull/19235\n* Defer inactive browser pages across worktree switches by @AmethystLiang in https://github.com/stablyai/orca/pull/19326\n* fix(native-chat): stop an unanswered host from reading as one that refuses structured chat by @brennanb2025 in https://github.com/stablyai/orca/pull/19321\n* fix(native-chat): scope composer file drops to the pane that received them by @brennanb2025 in https://github.com/stablyai/orca/pull/19328\n* revert(mobile): pull the relay connect-speed mobile pass pending a smaller, verified re-land by @Jinwoo-H in https://github.com/stablyai/orca/pull/19348\n* fix(relay): never cache a region hint from a one-region catalog by @Jinwoo-H in https://github.com/stablyai/orca/pull/19349\n* Show native chat message timestamps on hover by @brennanb2025 in https://github.com/stablyai/orca/pull/19218\n* feat(native-chat): add execution details and tool row identity by @brennanb2025 in https://github.com/stablyai/orca/pull/19226\n* chore(mobile): remove stale max-lines exceptions by @nwparker in https://github.com/stablyai/orca/pull/19366\n* fix(native-chat): render compaction notices, plan documents, and images by @brennanb2025 in https://github.com/stablyai/orca/pull/19228\n* fix(sidebar): reveal collapsed workspaces without clearing filters by @nwparker in https://github.com/stablyai/orca/pull/19398\n* fix(orchestration): avoid full message scans on working terminal titles by @brennanb2025 in https://github.com/stablyai/orca/pull/19390\n* fix(deps): update desktop parsers for security and bounded resource use by @OrcaWin in https://github.com/stablyai/orca/pull/19361\n* fix(deps): harden cloud HTTP, WebSocket and build dependencies by @OrcaWin in https://github.com/stablyai/orca/pull/19362\n* chore(mobile): patch XML parser security in plist tooling by @OrcaWin in https://github.com/stablyai/orca/pull/19380\n* chore(docs): patch brace expansion resource exhaustion fixes by @OrcaWin in https://github.com/stablyai/orca/pull/19382\n* fix(deps): update react-i18next for TypeScript 7 and translation parsing by @OrcaWin in https://github.com/stablyai/orca/pull/19378\n* fix(deps): update DOMPurify for sanitizer and document-context fixes by @OrcaWin in https://github.com/stablyai/orca/pull/19377\n* fix(ci): read the changed-path list past the first pipe buffer by @nwparker in https://github.com/stablyai/orca/pull/19409\n* fix(xterm): remove scrollback decorations by identity by @bbingz in https://github.com/stablyai/orca/pull/13178\n* feat(native-chat): read a tool batch as a group by @brennanb2025 in https://github.com/stablyai/orca/pull/19372\n* fix(deps): upgrade Electron to 43.6 for startup performance and crash fixes by @OrcaWin in https://github.com/stablyai/orca/pull/19369\n* fix: preserve user input during terminal scrollback replay by @nwparker in https://github.com/stablyai/orca/pull/19075\n* Unify sidebar create actions into single dropdown menu by @AmethystLiang in https://github.com/stablyai/orca/pull/19375\n* fix: preserve terminal retirement proof across renderer publications by @nwparker in https://github.com/stablyai/orca/pull/19002\n* perf(renderer): avoid per-second spinner animation events by @OrcaWin in https://github.com/stablyai/orca/pull/19407\n* fix(pi): show input modals as waiting instead of working by @mmarabel in https://github.com/stablyai/orca/pull/18836\n* Fix sidebar create menu E2E races by @AmethystLiang in https://github.com/stablyai/orca/pull/19448\n* test: cover input in five simultaneously flooding SSH panes by @nwparker in https://github.com/stablyai/orca/pull/19071\n* fix(runtime): stop a first status publication retiring in-flight worktree scans by @nwparker in https://github.com/stablyai/orca/pull/19357\n* fix(clipboard): route runtime-owned SSH image paste through the runtime by @nwparker in https://github.com/stablyai/orca/pull/19352\n* fix(sidebar): label pinned rows with their host on a multi-host sidebar by @nwparker in https://github.com/stablyai/orca/pull/19351\n* Log error details on session kill failure by @AmethystLiang in https://github.com/stablyai/orca/pull/19381\n* Stabilize scrollbar gutter to prevent message list layout shift by @AmethystLiang in https://github.com/stablyai/orca/pull/19332\n* Rank activity status groups by attention level by @AmethystLiang in https://github.com/stablyai/orca/pull/19329\n* perf: index selected team IDs and choose primary team in one pass by @OrcaWin in https://github.com/stablyai/orca/pull/19504\n* perf: skip Git-status indexing without eligible editors by @OrcaWin in https://github.com/stablyai/orca/pull/19437\n* perf: stop filesystem authorization at the first matching root by @OrcaWin in https://github.com/stablyai/orca/pull/19438\n* perf: index editor ownership and restored workspace projections by @OrcaWin in https://github.com/stablyai/orca/pull/19444\n* fix: avoid duplicate repository groups during catalog refresh by @kiendle in https://github.com/stablyai/orca/pull/19170\n* Fix native PTY I/O failures disabling session termination by @AmethystLiang in https://github.com/stablyai/orca/pull/19523\n* perf: index project table option and iteration order by @OrcaWin in https://github.com/stablyai/orca/pull/19476\n* perf: stop clone URL discovery at the first usable source by @OrcaWin in https://github.com/stablyai/orca/pull/19477\n* perf: clone only changed work-item pages by @OrcaWin in https://github.com/stablyai/orca/pull/19478\n* perf: use counted membership for worker transcript roster twins by @OrcaWin in https://github.com/stablyai/orca/pull/19484\n* perf: skip sorting when all final automation runs fit by @OrcaWin in https://github.com/stablyai/orca/pull/19485\n* perf: parse Git history headers without splitting commit bodies by @OrcaWin in https://github.com/stablyai/orca/pull/19486\n* perf: index forgotten Codex turns for bounded ordinal retention by @OrcaWin in https://github.com/stablyai/orca/pull/19488\n* perf: cache matched and unmatched Claude usage cwd attribution by @OrcaWin in https://github.com/stablyai/orca/pull/19489\n* perf: sum omitted workspace sizes without intermediate objects by @OrcaWin in https://github.com/stablyai/orca/pull/19491\n* fix(activity): show working agents under \"unread only\" by giving working turns a read receipt by @AmethystLiang in https://github.com/stablyai/orca/pull/19535\n* test: fix automation and browser reconciliation e2e tests by @AmethystLiang in https://github.com/stablyai/orca/pull/19530\n* perf: index VM feature restoration and precompute sorting identities by @OrcaWin in https://github.com/stablyai/orca/pull/19457\n* perf: reuse normalized path matchers for batch deletion by @OrcaWin in https://github.com/stablyai/orca/pull/19458\n* perf: normalize only retained browser history candidates by @OrcaWin in https://github.com/stablyai/orca/pull/19460\n* perf: index prior memberships during project identity succession by @OrcaWin in https://github.com/stablyai/orca/pull/19463\n* perf: precompute Jira priority and timestamp sorting keys by @OrcaWin in https://github.com/stablyai/orca/pull/19472\n* perf: parse external automation dates once per run by @OrcaWin in https://github.com/stablyai/orca/pull/19474\n* Verify failure causality in PR checks fix prompt before making changes by @AmethystLiang in https://github.com/stablyai/orca/pull/19435\n* fix(remote): keep terminal tabs syncing after orphan recovery by @shaharmor in https://github.com/stablyai/orca/pull/19065\n* perf(native-chat): bound journal reads during paged catch-up by @nwparker in https://github.com/stablyai/orca/pull/19360\n* perf(native-chat): preserve historical tool rows while streaming by @nwparker in https://github.com/stablyai/orca/pull/19364\n* perf: memoize ancestry when selecting foreground agents by @OrcaWin in https://github.com/stablyai/orca/pull/19502\n* fix(terminal): fold-safe CAN/SUB and double-ESC handling in partial-escape tail by @nwparker in https://github.com/stablyai/orca/pull/19521\n* perf: lazily index case-insensitive Windows environment keys by @OrcaWin in https://github.com/stablyai/orca/pull/19483\n* perf: select highest usage totals without full sorting by @OrcaWin in https://github.com/stablyai/orca/pull/19490\n* fix(agent-hooks): guard every Windows missing-target fallback before it reads stdin by @nwparker in https://github.com/stablyai/orca/pull/19415\n* perf: count GitLab diff line prefixes without splitting all lines by @OrcaWin in https://github.com/stablyai/orca/pull/19505\n* fix(terminal): remove host-retired ghost panes in paired remote splits (#17770) by @nwparker in https://github.com/stablyai/orca/pull/19365\n* feat(terminal): make the contrast floor user-configurable (#10754) by @nwparker in https://github.com/stablyai/orca/pull/18126\n* fix(orchestration): file federated worker mail under the coordinator Run by @Jinwoo-H in https://github.com/stablyai/orca/pull/19542\n* fix(orchestration): refuse Task re-open under a live worker; allow stop re-issue on a stranded row by @Jinwoo-H in https://github.com/stablyai/orca/pull/19551\n* fix(i18n): drop orphan TerminalPane.minimumContrast entries that broke main static analysis by @nwparker in https://github.com/stablyai/orca/pull/19566\n* Remove settled-worker automatic resume and hibernation fences by @Jinwoo-H in https://github.com/stablyai/orca/pull/19544\n* fix(pi): finish the dialog-wait signal on every surface by @nwparker in https://github.com/stablyai/orca/pull/19533\n* Organize activity menu into filter and view sections by @AmethystLiang in https://github.com/stablyai/orca/pull/19547\n* fix(orchestration): own a worker terminal from creation, not after the boot wait by @Jinwoo-H in https://github.com/stablyai/orca/pull/19608\n* fix(orchestration): fence worker release on mobile keystrokes by @Jinwoo-H in https://github.com/stablyai/orca/pull/19337\n* fix(native-chat): place effort to the right of model picker by @brennanb2025 in https://github.com/stablyai/orca/pull/19617\n* Show picker-selected native chat skills as pills by @brennanb2025 in https://github.com/stablyai/orca/pull/19616\n* fix(native-chat): reveal message chrome on keyboard focus, not any focus by @brennanb2025 in https://github.com/stablyai/orca/pull/19426\n* Unify native chat and workspace tab command dispatch by @brennanb2025 in https://github.com/stablyai/orca/pull/19621\n* fix(browser): load newly opened background tabs by @brennanb2025 in https://github.com/stablyai/orca/pull/19633\n* fix(renderer): align native chat pane layering by @brennanb2025 in https://github.com/stablyai/orca/pull/19626\n* Summarize turn file changes and preserve resolved prompt receipts by @brennanb2025 in https://github.com/stablyai/orca/pull/19229\n* perf: track pane alias singleton or ambiguity without copying buckets by @OrcaWin in https://github.com/stablyai/orca/pull/19479\n* perf: preserve store state on unchanged document titles by @OrcaWin in https://github.com/stablyai/orca/pull/19459\n* perf: index document history workspace and path identities by @OrcaWin in https://github.com/stablyai/orca/pull/19461\n* perf: index unified tabs for browser palette entries by @OrcaWin in https://github.com/stablyai/orca/pull/19464\n* perf: append to owned project source lists without repeated copying by @OrcaWin in https://github.com/stablyai/orca/pull/19465\n* perf: lazily index workspace tabs during terminal binding replay by @OrcaWin in https://github.com/stablyai/orca/pull/19466\n* perf: retire exhausted buckets from host-balanced listings by @OrcaWin in https://github.com/stablyai/orca/pull/19493\n* perf: replay pending chat asks with a FIFO cursor by @OrcaWin in https://github.com/stablyai/orca/pull/19496\n* perf: cache same-axis pane subtree weights during equalization by @OrcaWin in https://github.com/stablyai/orca/pull/19500\n* perf: group chat history sequences lazily within result windows by @OrcaWin in https://github.com/stablyai/orca/pull/19501\n* perf: measure sidebar row geometry once before sorting delete anchors by @OrcaWin in https://github.com/stablyai/orca/pull/19503\n* perf: concatenate only carried JSONL line prefixes by @OrcaWin in https://github.com/stablyai/orca/pull/19506\n* perf: index discovered skill IDs and names for batch selection by @OrcaWin in https://github.com/stablyai/orca/pull/19507\n* perf: skip fuzzy ranking when exact file matches fill the window by @OrcaWin in https://github.com/stablyai/orca/pull/19509\n* perf: index tab membership when rebasing authoritative sessions by @OrcaWin in https://github.com/stablyai/orca/pull/19510\n* perf: reuse collators when sorting discovered skills by @OrcaWin in https://github.com/stablyai/orca/pull/19440\n* perf: reuse collators when scanning Warp themes by @OrcaWin in https://github.com/stablyai/orca/pull/19441\n* perf: avoid repeated pending SOCKS payload copies by @OrcaWin in https://github.com/stablyai/orca/pull/19439\n* perf(terminal): avoid quadratic status-marker scans in output bursts by @nwparker in https://github.com/stablyai/orca/pull/19373\n* perf: index selected-host SSH leases by PTY by @OrcaWin in https://github.com/stablyai/orca/pull/19467\n* perf: index selected ancestor coverage during sidebar drag by @OrcaWin in https://github.com/stablyai/orca/pull/19443\n* perf: index Resource Manager labels and accumulated workspace rows by @OrcaWin in https://github.com/stablyai/orca/pull/19480\n* perf: drop oversized diagnostic records before retaining serialized text by @OrcaWin in https://github.com/stablyai/orca/pull/19445\n* perf: index selected skills when reporting bundle failures by @OrcaWin in https://github.com/stablyai/orca/pull/19481\n* perf: bound tool pairing and avoid unused attribution arrays by @OrcaWin in https://github.com/stablyai/orca/pull/19468\n* perf: scope activation inventory to the owning host and workspace by @OrcaWin in https://github.com/stablyai/orca/pull/19447\n* perf: index closed browser page IDs during focus cleanup by @OrcaWin in https://github.com/stablyai/orca/pull/19449\n* perf: prune metadata caches only when an entry can expire by @OrcaWin in https://github.com/stablyai/orca/pull/19450\n* perf: index observable skill installations by locked name by @OrcaWin in https://github.com/stablyai/orca/pull/19451\n* perf: skip folder-scope construction for separate repository imports by @OrcaWin in https://github.com/stablyai/orca/pull/19452\n* perf: validate terminal adoption MRU membership with group sets by @OrcaWin in https://github.com/stablyai/orca/pull/19482\n* perf: index terminal neighbor order and remaining membership by @OrcaWin in https://github.com/stablyai/orca/pull/19453\n* perf: index usage session breakdowns during aggregation and merge by @OrcaWin in https://github.com/stablyai/orca/pull/19487\n* perf: reuse AI-vault path matchers and query and sort keys by @OrcaWin in https://github.com/stablyai/orca/pull/19469\n* perf: cache AI-vault project attribution by host and cwd by @OrcaWin in https://github.com/stablyai/orca/pull/19470\n* perf: select checks-panel workspace attribution in one pass by @OrcaWin in https://github.com/stablyai/orca/pull/19471\n* perf: cache update timestamps for Linear and Jira result sorting by @OrcaWin in https://github.com/stablyai/orca/pull/19473\n* perf: reuse naturally ordered unique Codex trust ranges by @OrcaWin in https://github.com/stablyai/orca/pull/19492\n* perf: count command-line escapes without regex match arrays by @OrcaWin in https://github.com/stablyai/orca/pull/19499\n* perf: check backfill date cardinality before expanding ranges by @OrcaWin in https://github.com/stablyai/orca/pull/19456\n* perf: reuse normalized task-page repository selection by @OrcaWin in https://github.com/stablyai/orca/pull/19454\n* fix(native-chat): unify launch routing and support structured worker placement by @brennanb2025 in https://github.com/stablyai/orca/pull/19431\n* feat(native-chat): show Claude subagent activity on the shared carrier by @brennanb2025 in https://github.com/stablyai/orca/pull/18806\n* fix(orchestration): unify structured-session status across worktree ps and sidebar by @brennanb2025 in https://github.com/stablyai/orca/pull/19217\n* fix(native-chat): expand Claude slash commands sent with an attachment by @brennanb2025 in https://github.com/stablyai/orca/pull/19654\n* fix(native-chat): keep collapsible chevrons beside their header text by @brennanb2025 in https://github.com/stablyai/orca/pull/19656\n* fix(orchestration): worker-start settles readiness on observed turn start, not write acceptance by @brennanb2025 in https://github.com/stablyai/orca/pull/19423\n* test(orchestration): pin the Run-required contract for unbound direct mail by @brennanb2025 in https://github.com/stablyai/orca/pull/19684\n* fix(orchestration): accept a missing Run id from older federation coordinators by @brennanb2025 in https://github.com/stablyai/orca/pull/19689\n* Render task checklists with update diffs and a composer progress panel by @brennanb2025 in https://github.com/stablyai/orca/pull/19230\n* fix(lint): preserve deliberate collator benchmark baselines by @brennanb2025 in https://github.com/stablyai/orca/pull/19686\n* fix(native-chat): merge duplicate native-chat-types import by @Jinwoo-H in https://github.com/stablyai/orca/pull/19698\n* fix(orchestration): file mail from terminals in no Run under an unbound Run by @Jinwoo-H in https://github.com/stablyai/orca/pull/19696\n* feat(native-chat): report Codex background tasks in the chat strip by @brennanb2025 in https://github.com/stablyai/orca/pull/19346\n* Unify tab surface selection across workspace activation by @brennanb2025 in https://github.com/stablyai/orca/pull/19635\n* refactor(ai-vault): split the session scanner into a transcript reader and consumers by @Jinwoo-H in https://github.com/stablyai/orca/pull/19666\n* perf: stop queued metadata work after watcher cancellation by @OrcaWin in https://github.com/stablyai/orca/pull/19446\n* perf(runtime): skip hibernation inventories without completed agents by @nwparker in https://github.com/stablyai/orca/pull/19391\n* perf(workspaces): skip unchanged heartbeat subscriber work by @nwparker in https://github.com/stablyai/orca/pull/19392\n* perf: probe requested pane keys instead of enumerating records by @OrcaWin in https://github.com/stablyai/orca/pull/19494\n* perf: resolve current-workspace document addresses before catalog scans by @OrcaWin in https://github.com/stablyai/orca/pull/19495\n* perf: materialize only the requested recent plugin audit lines by @OrcaWin in https://github.com/stablyai/orca/pull/19498\n* test(ci): replace fixed teardown waits with bounded polls by @nwparker in https://github.com/stablyai/orca/pull/19720\n* perf: stop merging OS-opened files at the pending queue cap by @OrcaWin in https://github.com/stablyai/orca/pull/19508\n* perf(terminal): skip plain text between partial escape sequences by @nwparker in https://github.com/stablyai/orca/pull/19393\n* perf(terminal): release oversized backing strings behind pending controls by @nwparker in https://github.com/stablyai/orca/pull/19396\n* perf(terminal): skip ordinary output in preview control scans by @nwparker in https://github.com/stablyai/orca/pull/19400\n* perf(terminal): own retained tail rows so they stop pinning their chunks by @nwparker in https://github.com/stablyai/orca/pull/19528\n* perf: check tunnel queue capacity before copying frame payloads by @OrcaWin in https://github.com/stablyai/orca/pull/19497\n* Replace the sidebar create dropdown with two direct action buttons by @AmethystLiang in https://github.com/stablyai/orca/pull/19653\n* fix(native-chat): keep worktree active during chat creation by @Jinwoo-H in https://github.com/stablyai/orca/pull/19753\n* fix(push): isolate deployment and validate candidates before activation by @Jinwoo-H in https://github.com/stablyai/orca/pull/19771\n* fix(native-chat): show Claude working from the send, not the provider echo by @brennanb2025 in https://github.com/stablyai/orca/pull/19822\n* fix(mobile): enable patch-free iOS text selection in native chat by @brennanb2025 in https://github.com/stablyai/orca/pull/19769\n* perf(terminal): resume OSC terminator search past the carried frame by @nwparker in https://github.com/stablyai/orca/pull/19839\n* perf(native-chat): bound retained items on the structured session path by @nwparker in https://github.com/stablyai/orca/pull/19841\n* perf(terminal): share one ESC dispatch table between the two scanners by @nwparker in https://github.com/stablyai/orca/pull/19842\n* fix(native-chat): one `/` picker for every agent, anywhere in the prompt by @brennanb2025 in https://github.com/stablyai/orca/pull/19832\n* feat(native-chat): the background-tasks strip says what is running by @brennanb2025 in https://github.com/stablyai/orca/pull/19311\n* fix(i18n): drop the stale TerminalPane.minimumContrast entries from the runtime catalog (main red again) by @nwparker in https://github.com/stablyai/orca/pull/19575\n* fix(native-chat): keep an older page from punching a hole in the transcript by @nwparker in https://github.com/stablyai/orca/pull/19845\n* refactor(agent-status): publish structured sessions into the hook server store by @brennanb2025 in https://github.com/stablyai/orca/pull/19683\n* fix(orchestration): scope @ group addresses to the sender's Run by @Jinwoo-H in https://github.com/stablyai/orca/pull/19783\n* feat(native-chat): show live background work and name each row by kind by @brennanb2025 in https://github.com/stablyai/orca/pull/19705\n* feat(sidebar): show native-chat subagents as sidebar child rows, like CLI agents already do by @brennanb2025 in https://github.com/stablyai/orca/pull/19807\n* feat(native-chat): focus the message box when a chat appears by @brennanb2025 in https://github.com/stablyai/orca/pull/19868\n* fix(relay): keep failed rehome polls out of the durable failure budget by @Jinwoo-H in https://github.com/stablyai/orca/pull/19915\n* fix(orchestration): revalidate an attempted Enter instead of resending it by @brennanb2025 in https://github.com/stablyai/orca/pull/19911\n* test(native-chat): split structured question fixtures by @brennanb2025 in https://github.com/stablyai/orca/pull/19924\n* Make the structured turn lifecycle row durable so completed durations survive by @brennanb2025 in https://github.com/stablyai/orca/pull/19695\n* refactor(native-chat): unify agent session launch and open drafts in structured chat by @brennanb2025 in https://github.com/stablyai/orca/pull/19681\n* feat(cloud): native push gateway and dedicated infrastructure (1/3) by @Jinwoo-H in https://github.com/stablyai/orca/pull/19912\n* fix(browser): restore the Chrome-shaped browser identity (STA-7147) by @brennanb2025 in https://github.com/stablyai/orca/pull/19927\n* refactor(mobile): extract settings, diagnostics and editor-document screens from their routes by @Jinwoo-H in https://github.com/stablyai/orca/pull/19675\n* fix(native-chat): never adopt an unlisted model as the launch default by @brennanb2025 in https://github.com/stablyai/orca/pull/19854\n* fix(native-chat): settle a structured send on admission, not on the provider echo by @brennanb2025 in https://github.com/stablyai/orca/pull/19863\n* feat(desktop): native mobile push integration (2/3) by @Jinwoo-H in https://github.com/stablyai/orca/pull/19935\n* fix(sidebar): let an agent row show the provider's own session title by @brennanb2025 in https://github.com/stablyai/orca/pull/19936\n* fix(editor): make markdown images inline so a paragraph stays schema-valid by @OrcaWin in https://github.com/stablyai/orca/pull/19746\n* feat(native-chat): record the provider's name for an agent session by @brennanb2025 in https://github.com/stablyai/orca/pull/19908\n* fix(release): port three release-gate fixes to main so cuts stop re-inheriting them by @brennanb2025 in https://github.com/stablyai/orca/pull/19945\n* refactor(renderer): share path head elision by @AmethystLiang in https://github.com/stablyai/orca/pull/19938\n* feat(cmd-j): compact palette location layout by @AmethystLiang in https://github.com/stablyai/orca/pull/19939\n* refactor(mobile): name the RPC acceptance policies call sites hand-rolled by @Jinwoo-H in https://github.com/stablyai/orca/pull/19960\n* fix: prompt unexpectedly signed-out Cloud users once per version by @Jinwoo-H in https://github.com/stablyai/orca/pull/19966\n* feat(ai-vault-search): session search index as a transcript reader consumer by @Jinwoo-H in https://github.com/stablyai/orca/pull/19687\n* fix(native-chat): pass agent-implemented slash commands through to the agent by @brennanb2025 in https://github.com/stablyai/orca/pull/19929\n* perf(terminal): stop rebuilding per-workspace collections on a worktree switch by @nwparker in https://github.com/stablyai/orca/pull/19975\n* feat(rpc): generate a shared params catalog from the host registry, gated on parse parity by @Jinwoo-H in https://github.com/stablyai/orca/pull/19961\n* feat(native-chat): describe slash commands from the provider's own report by @brennanb2025 in https://github.com/stablyai/orca/pull/19928\n* perf(terminal): stop spending reveal-restore frames on panes that replay nothing by @nwparker in https://github.com/stablyai/orca/pull/19972\n* i18n: Make file reveal labels translatable by @OrcaWin in https://github.com/stablyai/orca/pull/20010\n* perf(native-chat): mount only the transcript rows near the viewport by @brennanb2025 in https://github.com/stablyai/orca/pull/19869\n* feat(mobile): receive native push notifications from paired desktops by @Jinwoo-H in https://github.com/stablyai/orca/pull/19951\n* fix(workspaces): seed shells only for blank selection by @brennanb2025 in https://github.com/stablyai/orca/pull/19940\n* Hide desktop theme imports from paired web clients by @OrcaWin in https://github.com/stablyai/orca/pull/20015\n* feat(ai-vault-search): session search query engine over the index by @Jinwoo-H in https://github.com/stablyai/orca/pull/19750\n* refactor(shared): derive WellKnownAgentType from TuiAgent by @AmethystLiang in https://github.com/stablyai/orca/pull/19645\n* Revert \"feat(ai-vault-search): session search query engine over the index (#19750)\" by @Jinwoo-H in https://github.com/stablyai/orca/pull/20023\n* feat(ai-vault-search): session search indexer library owning backfill, reconcile and retention by @Jinwoo-H in https://github.com/stablyai/orca/pull/19751\n* refactor(rpc): make defineMethod preserve the method name and handler result by @Jinwoo-H in https://github.com/stablyai/orca/pull/20016\n* fix(daemon): pin owner-only modes on persisted terminal history by @nwparker in https://github.com/stablyai/orca/pull/19954\n* Revert mobile push rollout pending delivery investigation by @Jinwoo-H in https://github.com/stablyai/orca/pull/20040\n* fix(pi): retire stale spinner and prompt timers across reloads by @nwparker in https://github.com/stablyai/orca/pull/20035\n* fix(terminal): stop the recovery budget erasing itself during a remount by @OrcaWin in https://github.com/stablyai/orca/pull/19745\n* fix(monaco): bound embedded-language recursion in svelte/astro/vue grammars by @OrcaWin in https://github.com/stablyai/orca/pull/19748\n* fix(runtime): agent-neutral wait-blocked reasons by @OrcaWin in https://github.com/stablyai/orca/pull/19749\n* fix(worktrees): close an idle structured chat on delete instead of refusing by @brennanb2025 in https://github.com/stablyai/orca/pull/19762\n* fix(runtime): let connections own host status recovery by @Jinwoo-H in https://github.com/stablyai/orca/pull/20003\n* fix(native-chat): show one live-turn indicator, and make Thinking mean reasoning by @brennanb2025 in https://github.com/stablyai/orca/pull/19977\n* perf(terminal): mount only the visible pane on a worktree switch by @nwparker in https://github.com/stablyai/orca/pull/20034\n* test(monaco): drive the real Monarch tokenizer instead of walking rule tables by @nwparker in https://github.com/stablyai/orca/pull/19981\n* test(runtime): capture real Antigravity transcripts — the detector is inverted on live output by @nwparker in https://github.com/stablyai/orca/pull/19983\n* fix(pi): claim the status pane when the inherited owner PID is dead (STA-5245) by @brennanb2025 in https://github.com/stablyai/orca/pull/16631\n* fix(pi): load extension providers for source control generation by @nwparker in https://github.com/stablyai/orca/pull/20070\n* fix(terminal): keep a deliberately slept workspace cold until it is woken by @nwparker in https://github.com/stablyai/orca/pull/20075\n* perf(daemon): stop scanning every cell for OSC links that cannot exist by @nwparker in https://github.com/stablyai/orca/pull/20077\n* fix(terminal): move the recovery ledger onto the tab row and gate it on observed outcome by @nwparker in https://github.com/stablyai/orca/pull/20025\n* fix(push): preserve distinct Android alerts while offline by @Jinwoo-H in https://github.com/stablyai/orca/pull/20066\n* feat(native-chat): show when a Codex goal is set, changed, or cleared by @brennanb2025 in https://github.com/stablyai/orca/pull/19923\n* Fix remote hosted review browser routing by @Jinwoo-H in https://github.com/stablyai/orca/pull/20030\n* fix(mobile): surface host create warnings and terminal-create errors by @brennanb2025 in https://github.com/stablyai/orca/pull/20125\n* feat(relay): correct regional placement only when the source is idle by @Jinwoo-H in https://github.com/stablyai/orca/pull/20105\n* test(relay): harden regional rehome race coverage by @Jinwoo-H in https://github.com/stablyai/orca/pull/20136\n* feat(desktop): measure relay regions and reconnect after idle cutover by @Jinwoo-H in https://github.com/stablyai/orca/pull/20106\n* refactor(agent-status): delete the runtime's retained row store (PR 1b) by @brennanb2025 in https://github.com/stablyai/orca/pull/19785\n* fix(deps): take Electron 43.7.0 for the glibc environ use-after-free (#20081) by @nwparker in https://github.com/stablyai/orca/pull/20089\n* fix(browser): decode Chromium SameSite storage values (135 cookies silently lost per profile) by @brennanb2025 in https://github.com/stablyai/orca/pull/20076\n* fix(terminal): re-run deferred tab admission when a plan installs mid-activation by @brennanb2025 in https://github.com/stablyai/orca/pull/20176\n* fix(workspaces): complete a worktree create when a post-create step throws by @brennanb2025 in https://github.com/stablyai/orca/pull/20175\n* feat(relay): support protocol 3 in cell rollout gates by @Jinwoo-H in https://github.com/stablyai/orca/pull/20174\n* fix(e2e): remove four real flake sources and one caret race by @brennanb2025 in https://github.com/stablyai/orca/pull/20169\n* fix(source-control): pass the Antigravity prompt to agy --print by @nwparker in https://github.com/stablyai/orca/pull/20147\n* fix(e2e): run worktree first-paint probe on a mapped window by @brennanb2025 in https://github.com/stablyai/orca/pull/20197\n* fix(relay): bound idle rehome polling and skip disabled scans by @Jinwoo-H in https://github.com/stablyai/orca/pull/20203\n* fix(hooks): stop orphaned managed markers from consuming user TOML by @nwparker in https://github.com/stablyai/orca/pull/20148\n* fix(codex): reconcile marketplace and plugin tables through the config mirror by @nwparker in https://github.com/stablyai/orca/pull/20150\n* fix(relay): reuse canary across completed rollout batches by @Jinwoo-H in https://github.com/stablyai/orca/pull/20214\n* Restore mobile push and fix cold-start dismissals by @Jinwoo-H in https://github.com/stablyai/orca/pull/20068\n* fix(cloud): tolerate sparse director errors in rollout monitor by @Jinwoo-H in https://github.com/stablyai/orca/pull/20238\n* feat(ai-vault-search): session search query engine over the index by @Jinwoo-H in https://github.com/stablyai/orca/pull/20029\n* feat(mobile): add typed RPC operations and fence raw requests by @Jinwoo-H in https://github.com/stablyai/orca/pull/20018\n* fix(native-chat): shrink skill pill text by @brennanb2025 in https://github.com/stablyai/orca/pull/20254\n* fix(claude): refuse a structured model the provider does not list by @brennanb2025 in https://github.com/stablyai/orca/pull/19946\n* perf: defer closed menus to speed up cold worktree switching by @nwparker in https://github.com/stablyai/orca/pull/20311\n* test: replace fixed UI waits with observable readiness by @nwparker in https://github.com/stablyai/orca/pull/20369\n* ci: reuse immutable package setup across shutdown checks by @nwparker in https://github.com/stablyai/orca/pull/20368\n* perf: index Hermes session correlation instead of rescanning history by @nwparker in https://github.com/stablyai/orca/pull/20275\n* fix: prevent infinite loops in mobile Markdown rendering by @nwparker in https://github.com/stablyai/orca/pull/20313\n* perf: eliminate mobile review table delimiter backtracking by @nwparker in https://github.com/stablyai/orca/pull/20317\n* perf: eliminate repeated mobile Markdown link suffix scans by @nwparker in https://github.com/stablyai/orca/pull/20322\n* ci: narrow pnpm cache keys and shallow development checkouts by @nwparker in https://github.com/stablyai/orca/pull/20370\n* perf(browser): fill plain fields in one stdin edit by @nwparker in https://github.com/stablyai/orca/pull/20364\n* Fix landing footer menu placement by @nwparker in https://github.com/stablyai/orca/pull/20360\n* ci: balance existing unit and E2E shards using recorded timings by @nwparker in https://github.com/stablyai/orca/pull/20367\n* fix(cloud): diagnose wrapped relay trust probe failures by @Jinwoo-H in https://github.com/stablyai/orca/pull/20403\n* refactor(renderer): improve IPC error handling with clamped and unclamped variants by @AmethystLiang in https://github.com/stablyai/orca/pull/20340\n* Make sidebar nesting easier while preserving animated reordering by @nwparker in https://github.com/stablyai/orca/pull/20412\n* refactor(preload): drop the unused raw electron IPC bridge by @nwparker in https://github.com/stablyai/orca/pull/20419\n* fix(browser): move cookie scoping off psl's stale suffix list by @nwparker in https://github.com/stablyai/orca/pull/20421\n* fix(claude-accounts): stop path resolution from creating the config directory by @makoto-developer in https://github.com/stablyai/orca/pull/12309\n* perf: normalize title evidence once per label search by @nwparker in https://github.com/stablyai/orca/pull/20331\n* fix(runtime): GC expired proven-absent leaf PTY verdicts by @innocarpe in https://github.com/stablyai/orca/pull/12810\n* fix(ai-vault): close OpenCode handles when query_only setup fails by @bbingz in https://github.com/stablyai/orca/pull/14134\n* perf(wsl): release environment probe deadline timers by @OrcaWin in https://github.com/stablyai/orca/pull/20207\n* perf(daemon): bound immediate flush queue scans by @OrcaWin in https://github.com/stablyai/orca/pull/20209\n* perf(mobile): normalize history scope paths once per candidate by @nwparker in https://github.com/stablyai/orca/pull/20210\n* perf(automations): skip sorting count-only Hermes history by @OrcaWin in https://github.com/stablyai/orca/pull/20213\n* perf(chat): reuse streamed delta byte lengths by @OrcaWin in https://github.com/stablyai/orca/pull/20215\n* perf(files): skip unused Quick Open byte accounting by @OrcaWin in https://github.com/stablyai/orca/pull/20216\n* perf(relay): reuse host data attachment owner without inventory copies by @nwparker in https://github.com/stablyai/orca/pull/20219\n* perf(mobile): avoid materializing input characters on backspace by @OrcaWin in https://github.com/stablyai/orca/pull/20220\n* perf(browser): skip unneeded inventory compaction planning by @OrcaWin in https://github.com/stablyai/orca/pull/20221\n* perf(usage): skip session scans for empty breakdowns by @OrcaWin in https://github.com/stablyai/orca/pull/20222\n* perf(session): reuse persisted tab membership set by @OrcaWin in https://github.com/stablyai/orca/pull/20223\n* perf(diff): skip unused extension extraction during search by @OrcaWin in https://github.com/stablyai/orca/pull/20225\n* perf(mobile): bound autocomplete substring retention by @OrcaWin in https://github.com/stablyai/orca/pull/20226\n* perf: index pending terminal reconnect tab lookups by @OrcaWin in https://github.com/stablyai/orca/pull/20229\n* perf: skip ordinary strings in JSON structure checks by @OrcaWin in https://github.com/stablyai/orca/pull/20232\n* perf: avoid rescanning partial browser sidecar responses by @OrcaWin in https://github.com/stablyai/orca/pull/20242\n* perf: count local RPC request bytes incrementally by @OrcaWin in https://github.com/stablyai/orca/pull/20243\n* perf(history): cache numeric transcript timeline bounds by @OrcaWin in https://github.com/stablyai/orca/pull/20244\n* perf: stop heading lookup at the requested occurrence by @OrcaWin in https://github.com/stablyai/orca/pull/20246\n* perf: defer browser search row formatting until matched by @OrcaWin in https://github.com/stablyai/orca/pull/20248\n* perf: stop preparing over-limit palette queries by @OrcaWin in https://github.com/stablyai/orca/pull/20250\n* perf: avoid duplicate repository directory probes by @OrcaWin in https://github.com/stablyai/orca/pull/20253\n* perf: compile nested repository ignore patterns once by @OrcaWin in https://github.com/stablyai/orca/pull/20256\n* perf: avoid repeated model pricing name normalization by @OrcaWin in https://github.com/stablyai/orca/pull/20257\n* perf(skills): index source ownership counts per discovery by @OrcaWin in https://github.com/stablyai/orca/pull/20259\n* perf(plugins): stop collection traversal at message size limit by @nwparker in https://github.com/stablyai/orca/pull/20260\n* perf(history): scan legacy alternate-screen markers linearly by @OrcaWin in https://github.com/stablyai/orca/pull/20267\n* perf: reuse skill file digest when normalized bytes match by @OrcaWin in https://github.com/stablyai/orca/pull/20268\n* perf(native): avoid sanitizing snapshot values twice by @nwparker in https://github.com/stablyai/orca/pull/20271\n* perf: encode skill Git tree sort keys once by @OrcaWin in https://github.com/stablyai/orca/pull/20272\n* perf: skip decoding discarded diagnostic error bodies by @OrcaWin in https://github.com/stablyai/orca/pull/20276\n* perf: avoid flattening tabs for smart-sort liveness check by @OrcaWin in https://github.com/stablyai/orca/pull/20279\n* perf: accept UTF-8 text that fits the worst-case byte bound by @OrcaWin in https://github.com/stablyai/orca/pull/20286\n* perf: bound rich Markdown comment detection and preservation scans by @nwparker in https://github.com/stablyai/orca/pull/20287\n* perf(ipc): bound SSH ACK batch selection scans by @nwparker in https://github.com/stablyai/orca/pull/20288\n* perf: avoid copying completed terminal source ranges by @OrcaWin in https://github.com/stablyai/orca/pull/20289\n* perf(ssh): skip terminal source spans awaiting ACK publication by @OrcaWin in https://github.com/stablyai/orca/pull/20290\n* perf: reuse diagnostic bundle byte count by @OrcaWin in https://github.com/stablyai/orca/pull/20292\n* perf: normalize manifest text files once by @OrcaWin in https://github.com/stablyai/orca/pull/20294\n* perf: reuse formatter within reset timestamp conversion by @OrcaWin in https://github.com/stablyai/orca/pull/20295\n* perf: defer scrollback pruning indexes until needed by @OrcaWin in https://github.com/stablyai/orca/pull/20297\n* perf(daemon): reuse encoded unsplit NDJSON frames by @nwparker in https://github.com/stablyai/orca/pull/20298\n* perf(browser): index iframe session refs for snapshot lookup by @nwparker in https://github.com/stablyai/orca/pull/20300\n* perf: reuse computed project identity in projection by @OrcaWin in https://github.com/stablyai/orca/pull/20303\n* perf: stop archive size scan once over budget by @OrcaWin in https://github.com/stablyai/orca/pull/20304\n* perf: avoid per-item arrays in journal projection by @OrcaWin in https://github.com/stablyai/orca/pull/20305\n* perf: reuse Windows PATH normalization within merges by @OrcaWin in https://github.com/stablyai/orca/pull/20309\n* perf: find path basename without splitting all segments by @OrcaWin in https://github.com/stablyai/orca/pull/20312\n* perf(ssh): copy cleanup tab map only once by @OrcaWin in https://github.com/stablyai/orca/pull/20315\n* perf: fast-path Windows arguments without escapes by @OrcaWin in https://github.com/stablyai/orca/pull/20318\n* perf(checks): bound log excerpt byte-limit checks by @OrcaWin in https://github.com/stablyai/orca/pull/20319\n* perf: use native newline search in rich Markdown code stripping by @nwparker in https://github.com/stablyai/orca/pull/20320\n* perf: stop building chat diff sections beyond preview limit by @OrcaWin in https://github.com/stablyai/orca/pull/20321\n* perf: count chat tool calls without a filtered array by @OrcaWin in https://github.com/stablyai/orca/pull/20328\n* perf: reject file-count labels before parsing edit patches by @OrcaWin in https://github.com/stablyai/orca/pull/20330\n* perf(terminal): reuse bounded native snapshot byte checks by @OrcaWin in https://github.com/stablyai/orca/pull/20333\n* perf: select matching model IDs without sorting by @OrcaWin in https://github.com/stablyai/orca/pull/20334\n* perf: remove duplicate empty-file filtering from search finalization by @OrcaWin in https://github.com/stablyai/orca/pull/20335\n* perf: avoid repeated history page byte serialization by @nwparker in https://github.com/stablyai/orca/pull/20337\n* perf: collect terminal snapshot replay without wrapper arrays by @OrcaWin in https://github.com/stablyai/orca/pull/20338\n* perf: reuse accepted project group IDs for parent validation by @OrcaWin in https://github.com/stablyai/orca/pull/20341\n* perf: compute check severity once per sort by @OrcaWin in https://github.com/stablyai/orca/pull/20342\n* perf: count bot comments without a filtered array by @OrcaWin in https://github.com/stablyai/orca/pull/20343\n* perf: index relay drain migration inventories once by @nwparker in https://github.com/stablyai/orca/pull/20344\n* perf: avoid typed-array iteration in shared SHA-256 blocks by @OrcaWin in https://github.com/stablyai/orca/pull/20346\n* perf: reuse shared SHA-256 for renderer fallback by @OrcaWin in https://github.com/stablyai/orca/pull/20347\n* perf(computer): avoid rescanning fragmented native provider replies by @OrcaWin in https://github.com/stablyai/orca/pull/20348\n* perf: skip nonmatching spans in Quick Open scoring by @nwparker in https://github.com/stablyai/orca/pull/20349\n* perf: skip dominated quick command search fields by @nwparker in https://github.com/stablyai/orca/pull/20352\n* perf: reuse normalized executable during agent recognition by @OrcaWin in https://github.com/stablyai/orca/pull/20353\n* perf(localization): prefilter absent brands before regex matching by @OrcaWin in https://github.com/stablyai/orca/pull/20354\n* perf: avoid copying single-chunk process output by @OrcaWin in https://github.com/stablyai/orca/pull/20355\n* perf: scan chat activity text from its last line by @nwparker in https://github.com/stablyai/orca/pull/20356\n* perf(mobile): reuse UTF-8 prefix truncation for diagnostics by @OrcaWin in https://github.com/stablyai/orca/pull/20358\n* perf(checks): bound earlier error context collection by @OrcaWin in https://github.com/stablyai/orca/pull/20211\n* perf(mobile): reuse numeric collators across source control sorts by @nwparker in https://github.com/stablyai/orca/pull/20224\n* perf(mobile): precompute task sort keys and reuse repository collation by @nwparker in https://github.com/stablyai/orca/pull/20233\n* perf(agents): avoid cloning lineage ancestor sets by @OrcaWin in https://github.com/stablyai/orca/pull/20236\n* perf(plugins): reconcile shortcut conflicts once per owner by @nwparker in https://github.com/stablyai/orca/pull/20237\n* perf: avoid rescanning partial transcript records by @OrcaWin in https://github.com/stablyai/orca/pull/20239\n* perf: use native newline search for Markdown review excerpts by @nwparker in https://github.com/stablyai/orca/pull/20245\n* perf(ai-vault): deduplicate session scan batches incrementally by @OrcaWin in https://github.com/stablyai/orca/pull/20255\n* perf: stop review acknowledgement summaries at the first readable line by @nwparker in https://github.com/stablyai/orca/pull/20263\n* perf(git): classify bulk discard paths once by @nwparker in https://github.com/stablyai/orca/pull/20274\n* perf: eliminate quadratic palette phrase placement scans by @nwparker in https://github.com/stablyai/orca/pull/20306\n* perf: avoid repeated unclosed-tag scans in mobile reviews by @nwparker in https://github.com/stablyai/orca/pull/20329\n* perf(plugins): index contributed keybindings by command by @nwparker in https://github.com/stablyai/orca/pull/20241\n* perf(mobile): precompute Linear issue sort keys by @nwparker in https://github.com/stablyai/orca/pull/20249\n* perf(chat): join transcript line fragments at record boundaries by @OrcaWin in https://github.com/stablyai/orca/pull/20278\n* perf(cmd-j): scan phrase word boundaries monotonically by @nwparker in https://github.com/stablyai/orca/pull/20282\n* perf(ai-vault): count recent sessions for scan cutoffs by @OrcaWin in https://github.com/stablyai/orca/pull/20301\n* perf: remove repeated sibling scans from cyclic agent lineage cleanup by @nwparker in https://github.com/stablyai/orca/pull/20302\n* perf: reuse pending filesystem watcher debounce timers by @OrcaWin in https://github.com/stablyai/orca/pull/20264\n* fix(editor): eliminate multi-second Markdown blank-run scans by @nwparker in https://github.com/stablyai/orca/pull/20231\n* perf: avoid rescanning partial Windows desktop responses by @OrcaWin in https://github.com/stablyai/orca/pull/20235\n* fix(chat): bound journal replay memory by live history by @nwparker in https://github.com/stablyai/orca/pull/20247\n* perf(android): queue fragmented scrcpy video packets by @OrcaWin in https://github.com/stablyai/orca/pull/20230\n* perf(mobile): skip successful duplicate connection log saves by @nwparker in https://github.com/stablyai/orca/pull/20252\n* perf: skip impossible inline HTML comment matches during encoding by @nwparker in https://github.com/stablyai/orca/pull/20293\n* chore: remove 20.7 MiB of duplicate and unused media by @nwparker in https://github.com/stablyai/orca/pull/20416\n* perf: bound whitespace normalization for tool previews by @nwparker in https://github.com/stablyai/orca/pull/20332\n* fix(tabs): cancel split drags when the window loses focus by @OrcaWin in https://github.com/stablyai/orca/pull/20323\n* fix(updater): open background check errors from the status bar by @OrcaWin in https://github.com/stablyai/orca/pull/20270\n* perf(relay): sort latency samples once per percentile pair by @nwparker in https://github.com/stablyai/orca/pull/20427\n* perf(skills): test agent ownership without building a deduped list by @nwparker in https://github.com/stablyai/orca/pull/20428\n* perf(git): reject non-HEAD refs before sorting remotes by @nwparker in https://github.com/stablyai/orca/pull/20429\n* perf(mobile): release consumed terminal write-queue slots by @nwparker in https://github.com/stablyai/orca/pull/20430\n* perf(git): normalize tracked discard paths once per operation by @nwparker in https://github.com/stablyai/orca/pull/20299\n* refactor(windows): vendor the registry addon as @orca/windows-registry by @nwparker in https://github.com/stablyai/orca/pull/20438\n* fix(resource-manager): show saved folder workspace names and groups by @OrcaWin in https://github.com/stablyai/orca/pull/20324\n* test(relay): differential coverage for the single-pass host-data owner lookup by @nwparker in https://github.com/stablyai/orca/pull/20426\n* perf(mobile): reuse Linear grouping between list and board by @nwparker in https://github.com/stablyai/orca/pull/20431\n* chore(deps): pin serve-sim to an exact version by @nwparker in https://github.com/stablyai/orca/pull/20446\n* Add code quality lint for type assertions by @AmethystLiang in https://github.com/stablyai/orca/pull/19462\n* fix(ci): cache the vendored addon where node-gyp actually writes it by @nwparker in https://github.com/stablyai/orca/pull/20445\n* perf: accelerate cancellable remote transcript line scanning by @OrcaWin in https://github.com/stablyai/orca/pull/20351\n* fix(ci): match the truncated windows-process-tree virtual store dir by @nwparker in https://github.com/stablyai/orca/pull/20447\n* build(macos): parallelize native helpers with complete cancellation by @espetro in https://github.com/stablyai/orca/pull/19651\n* perf: skip WSL discovery when filtering native-only paths by @OrcaWin in https://github.com/stablyai/orca/pull/20266\n* perf(mobile): reuse Linear ordering for list and board grouping by @nwparker in https://github.com/stablyai/orca/pull/20314\n* Reduce native dependency installs to the host platform by @nwparker in https://github.com/stablyai/orca/pull/20420\n* perf(ai-vault): bound per-row bookkeeping in unlimited session scans by @nwparker in https://github.com/stablyai/orca/pull/20291\n* perf(tooling): reuse directory entry types in source scans by @OrcaWin in https://github.com/stablyai/orca/pull/20212\n* fix(skills): evict removed runtime discovery cache by @bbingz in https://github.com/stablyai/orca/pull/11489\n* fix(automations): isolate the scheduler tick and refuse oversized cron steps by @nwparker in https://github.com/stablyai/orca/pull/20152\n* fix(runtime): stop a name-only agent title from satisfying tui-idle by @nwparker in https://github.com/stablyai/orca/pull/20155\n* fix(agent-hooks): stop navigation Escape from inferring completion by @nwparker in https://github.com/stablyai/orca/pull/20149\n* fix(mobile): reuse current workspace on notification taps by @Jinwoo-H in https://github.com/stablyai/orca/pull/20310\n* fix(ai-vault): re-read a transcript rewritten to its previous size by @Jinwoo-H in https://github.com/stablyai/orca/pull/20261\n* fix(sidebar): show agent activity before workspace activation by @Jinwoo-H in https://github.com/stablyai/orca/pull/20398\n* test(rpc): add a compile-time params catalog parity gate by @Jinwoo-H in https://github.com/stablyai/orca/pull/20281\n* test(scripts): widen the Windows shim ratchet to catch package bin spawns by @Jinwoo-H in https://github.com/stablyai/orca/pull/20285\n* refactor(native-chat): give each structured dispatch state exactly one meaning by @brennanb2025 in https://github.com/stablyai/orca/pull/20133\n* feat(native-chat): decide a restart-stranded send against provider history by @brennanb2025 in https://github.com/stablyai/orca/pull/20139\n* fix(native-chat): wait for the runtime capability probe before resolving the creation launch route by @brennanb2025 in https://github.com/stablyai/orca/pull/19819\n* fix(worktrees): retire the chat tab of a chat with no child when its workspace goes by @brennanb2025 in https://github.com/stablyai/orca/pull/19970\n* feat(ai-vault-search): public session search contract and transports by @Jinwoo-H in https://github.com/stablyai/orca/pull/20277\n* refactor(mobile): migrate settings reads to RpcOperation by @Jinwoo-H in https://github.com/stablyai/orca/pull/20499\n* fix(mobile): give native chat one tail-follow owner so streaming stops jumping by @brennanb2025 in https://github.com/stablyai/orca/pull/20493\n* fix(mobile): preserve delivery ambiguity across transport cutover by @Jinwoo-H in https://github.com/stablyai/orca/pull/20280\n* fix(runtime): reject malformed file Base64 padding by @Jinwoo-H in https://github.com/stablyai/orca/pull/20283\n* fix(tasks): preserve repository results under GitHub search quota by @OrcaWin in https://github.com/stablyai/orca/pull/20460\n* fix(ai-vault): stream oversized remote session transcripts by @OrcaWin in https://github.com/stablyai/orca/pull/20455\n* fix(grok): stop replayed Claude/Cursor hooks reporting Grok panes as Claude by @brennanb2025 in https://github.com/stablyai/orca/pull/20507\n* fix(browser): press keys through a US-layout CDP key table instead of a subprocess per keystroke by @atreidesend in https://github.com/stablyai/orca/pull/15310\n* fix(rpc): validate provider-specific fields in TaskProviderIdentity by @Jinwoo-H in https://github.com/stablyai/orca/pull/20284\n* perf(terminal): batch file-link checks on their owning host by @OrcaWin in https://github.com/stablyai/orca/pull/20463\n* feat(native-chat): support file drag and drop by @brennanb2025 in https://github.com/stablyai/orca/pull/20494\n\n## New Contributors\n* @tb-soshiro made their first contribution in https://github.com/stablyai/orca/pull/18787\n* @NaoyaTatetsu made their first contribution in https://github.com/stablyai/orca/pull/17795\n* @ramarivera made their first contribution in https://github.com/stablyai/orca/pull/12772\n* @Aladex made their first contribution in https://github.com/stablyai/orca/pull/18634\n* @weekbin made their first contribution in https://github.com/stablyai/orca/pull/14929\n* @TimothyVang made their first contribution in https://github.com/stablyai/orca/pull/17936\n* @brohoya made their first contribution in https://github.com/stablyai/orca/pull/11756\n* @blade035 made their first contribution in https://github.com/stablyai/orca/pull/17731\n* @kiendle made their first contribution in https://github.com/stablyai/orca/pull/19170\n* @espetro made their first contribution in https://github.com/stablyai/orca/pull/19651\n* @atreidesend made their first contribution in https://github.com/stablyai/orca/pull/15310\n\n**Full Changelog**: https://github.com/stablyai/orca/compare/mobile-android-v0.0.48...mobile-android-v0.0.50\n", + "reactions": { + "url": "https://api.github.com/repos/stablyai/orca/releases/391254643/reactions", + "total_count": 18, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 14, + "rocket": 4, + "eyes": 0 + }, + "mentions_count": 21 +} diff --git a/mobile/src/app-update/fixtures/itunes-lookup-com.stably.orca.mobile.json b/mobile/src/app-update/fixtures/itunes-lookup-com.stably.orca.mobile.json new file mode 100644 index 00000000000..f4de826ef72 --- /dev/null +++ b/mobile/src/app-update/fixtures/itunes-lookup-com.stably.orca.mobile.json @@ -0,0 +1,189 @@ +{ + "resultCount": 1, + "results": [ + { + "isGameCenterEnabled": false, + "features": ["iosUniversal"], + "supportedDevices": [ + "iPhone5s-iPhone5s", + "iPadAir-iPadAir", + "iPadAirCellular-iPadAirCellular", + "iPadMiniRetina-iPadMiniRetina", + "iPadMiniRetinaCellular-iPadMiniRetinaCellular", + "iPhone6-iPhone6", + "iPhone6Plus-iPhone6Plus", + "iPadAir2-iPadAir2", + "iPadAir2Cellular-iPadAir2Cellular", + "iPadMini3-iPadMini3", + "iPadMini3Cellular-iPadMini3Cellular", + "iPodTouchSixthGen-iPodTouchSixthGen", + "iPhone6s-iPhone6s", + "iPhone6sPlus-iPhone6sPlus", + "iPadMini4-iPadMini4", + "iPadMini4Cellular-iPadMini4Cellular", + "iPadPro-iPadPro", + "iPadProCellular-iPadProCellular", + "iPadPro97-iPadPro97", + "iPadPro97Cellular-iPadPro97Cellular", + "iPhoneSE-iPhoneSE", + "iPhone7-iPhone7", + "iPhone7Plus-iPhone7Plus", + "iPad611-iPad611", + "iPad612-iPad612", + "iPad71-iPad71", + "iPad72-iPad72", + "iPad73-iPad73", + "iPad74-iPad74", + "iPhone8-iPhone8", + "iPhone8Plus-iPhone8Plus", + "iPhoneX-iPhoneX", + "iPad75-iPad75", + "iPad76-iPad76", + "iPhoneXS-iPhoneXS", + "iPhoneXSMax-iPhoneXSMax", + "iPhoneXR-iPhoneXR", + "iPad812-iPad812", + "iPad834-iPad834", + "iPad856-iPad856", + "iPad878-iPad878", + "iPadMini5-iPadMini5", + "iPadMini5Cellular-iPadMini5Cellular", + "iPadAir3-iPadAir3", + "iPadAir3Cellular-iPadAir3Cellular", + "iPodTouchSeventhGen-iPodTouchSeventhGen", + "iPhone11-iPhone11", + "iPhone11Pro-iPhone11Pro", + "iPadSeventhGen-iPadSeventhGen", + "iPadSeventhGenCellular-iPadSeventhGenCellular", + "iPhone11ProMax-iPhone11ProMax", + "iPhoneSESecondGen-iPhoneSESecondGen", + "iPadProSecondGen-iPadProSecondGen", + "iPadProSecondGenCellular-iPadProSecondGenCellular", + "iPadProFourthGen-iPadProFourthGen", + "iPadProFourthGenCellular-iPadProFourthGenCellular", + "iPhone12Mini-iPhone12Mini", + "iPhone12-iPhone12", + "iPhone12Pro-iPhone12Pro", + "iPhone12ProMax-iPhone12ProMax", + "iPadAir4-iPadAir4", + "iPadAir4Cellular-iPadAir4Cellular", + "iPadEighthGen-iPadEighthGen", + "iPadEighthGenCellular-iPadEighthGenCellular", + "iPadProThirdGen-iPadProThirdGen", + "iPadProThirdGenCellular-iPadProThirdGenCellular", + "iPadProFifthGen-iPadProFifthGen", + "iPadProFifthGenCellular-iPadProFifthGenCellular", + "iPhone13Pro-iPhone13Pro", + "iPhone13ProMax-iPhone13ProMax", + "iPhone13Mini-iPhone13Mini", + "iPhone13-iPhone13", + "iPadMiniSixthGen-iPadMiniSixthGen", + "iPadMiniSixthGenCellular-iPadMiniSixthGenCellular", + "iPadNinthGen-iPadNinthGen", + "iPadNinthGenCellular-iPadNinthGenCellular", + "iPhoneSEThirdGen-iPhoneSEThirdGen", + "iPadAirFifthGen-iPadAirFifthGen", + "iPadAirFifthGenCellular-iPadAirFifthGenCellular", + "iPhone14-iPhone14", + "iPhone14Plus-iPhone14Plus", + "iPhone14Pro-iPhone14Pro", + "iPhone14ProMax-iPhone14ProMax", + "iPadTenthGen-iPadTenthGen", + "iPadTenthGenCellular-iPadTenthGenCellular", + "iPadPro11FourthGen-iPadPro11FourthGen", + "iPadPro11FourthGenCellular-iPadPro11FourthGenCellular", + "iPadProSixthGen-iPadProSixthGen", + "iPadProSixthGenCellular-iPadProSixthGenCellular", + "iPhone15-iPhone15", + "iPhone15Plus-iPhone15Plus", + "iPhone15Pro-iPhone15Pro", + "iPhone15ProMax-iPhone15ProMax", + "iPadAir11M2-iPadAir11M2", + "iPadAir11M2Cellular-iPadAir11M2Cellular", + "iPadAir13M2-iPadAir13M2", + "iPadAir13M2Cellular-iPadAir13M2Cellular", + "iPadPro11M4-iPadPro11M4", + "iPadPro11M4Cellular-iPadPro11M4Cellular", + "iPadPro13M4-iPadPro13M4", + "iPadPro13M4Cellular-iPadPro13M4Cellular", + "iPhone16-iPhone16", + "iPhone16Plus-iPhone16Plus", + "iPhone16Pro-iPhone16Pro", + "iPhone16ProMax-iPhone16ProMax", + "iPadMiniA17Pro-iPadMiniA17Pro", + "iPadMiniA17ProCellular-iPadMiniA17ProCellular", + "iPhone16e-iPhone16e", + "iPadA16-iPadA16", + "iPadA16Cellular-iPadA16Cellular", + "iPadAir11M3-iPadAir11M3", + "iPadAir11M3Cellular-iPadAir11M3Cellular", + "iPadAir13M3-iPadAir13M3", + "iPadAir13M3Cellular-iPadAir13M3Cellular", + "iPhone17Pro-iPhone17Pro", + "iPhone17ProMax-iPhone17ProMax", + "iPhone17-iPhone17", + "iPhoneAir-iPhoneAir", + "iPadPro11M5-iPadPro11M5", + "iPadPro11M5Cellular-iPadPro11M5Cellular", + "iPadPro13M5-iPadPro13M5", + "iPadPro13M5Cellular-iPadPro13M5Cellular", + "iPhone17e-iPhone17e", + "iPadAir11M4-iPadAir11M4", + "iPadAir11M4Cellular-iPadAir11M4Cellular", + "iPadAir13M4-iPadAir13M4", + "iPadAir13M4Cellular-iPadAir13M4Cellular", + "iPhone18Pro-iPhone18Pro", + "iPhone18ProMax-iPhone18ProMax", + "iPhoneDuo-iPhoneDuo" + ], + "advisories": [], + "kind": "software", + "artistViewUrl": "https://apps.apple.com/us/developer/lovecast-llc/id1498017204?uo=4", + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/89/b3/29/89b32922-c086-9d30-fd7c-ac48e1392aa6/AppIcon-0-0-1x_U007epad-0-1-85-220.png/60x60bb.jpg", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/89/b3/29/89b32922-c086-9d30-fd7c-ac48e1392aa6/AppIcon-0-0-1x_U007epad-0-1-85-220.png/100x100bb.jpg", + "appletvScreenshotUrls": [], + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/89/b3/29/89b32922-c086-9d30-fd7c-ac48e1392aa6/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg", + "screenshotUrls": [ + "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/7d/c0/9e/7dc09e7d-e1df-c528-41c6-2eff0bac4b9b/01-terminal.png/320x480bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource221/v4/93/6f/1f/936f1fe8-a0fc-cfaa-2ecb-89d7c71f02f3/03-homepage.png/320x480bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource221/v4/d6/e2/9f/d6e29f00-bdd8-c29c-f533-97a4a217be03/02-workspace.png/320x480bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/3e/82/15/3e8215b7-80db-f4b3-3942-bc634899c394/04-create-wt.png/320x480bb.jpg" + ], + "ipadScreenshotUrls": [ + "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource221/v4/e5/01/a2/e501a2f6-8e2c-11a2-ad6a-c9b42f49e905/apple-ipadpro13-spacegrey-portrait-promo-opaque.png/360x480bb.jpg" + ], + "releaseNotes": "Clearer chat while agents work\n• A proposed plan now appears as a plan you can read, not a generic approval prompt.\n• Approval requests show what the agent actually asked for instead of raw tool data.\n• The working timer counts from the message that started the turn.\n\nSteadier workspace setup\n• Creating a workspace no longer risks starting two if the reply is lost on the way back.\n• A worktree that launches an agent on creation opens straight into its chat.\n• A newly created tab stays put when the host list refreshes.\n\nFixes\n• Pairing no longer reports a declined permission as a missing feature.\n• The notification opt-in screen is clearer about what you are turning on.\n• Sign-in now accepts enterprise-managed GitHub accounts.", + "bundleId": "com.stably.orca.mobile", + "genres": ["Developer Tools"], + "price": 0.0, + "artistId": 1498017204, + "artistName": "Lovecast LLC", + "primaryGenreName": "Developer Tools", + "primaryGenreId": 6026, + "releaseDate": "2026-05-07T07:00:00Z", + "isVppDeviceBasedLicensingEnabled": true, + "trackName": "Orca IDE", + "trackId": 6766130217, + "sellerName": "Lovecast LLC", + "genreIds": ["6026"], + "currentVersionReleaseDate": "2026-09-19T07:09:57Z", + "version": "0.0.51", + "wrapperType": "software", + "currency": "USD", + "description": "Orca Mobile is the companion app for Orca, the open-source agentic coding workspace for macOS, Linux, and Windows. Pair your iPhone to your desktop in seconds and pick up a terminal session from the couch, the kitchen, or across the office.\n\nWHAT IT DOES\n\n• Live mirror of your desktop terminal, including running AI agent sessions (Claude Code, Codex, custom CLIs).\n• Full keyboard input — type, tab-complete, Ctrl-C, scroll back, all from the phone.\n• Multi-worktree, multi-terminal: jump between projects with a tap.\n• Phone-fit zoom and a smart on-screen keyboard with quick Ctrl/Alt accessory keys and shortcuts.\n• Background long-running commands and get a notification when a TUI goes idle (e.g. when Claude finishes thinking).\n\nHOW IT WORKS\n\nOrca Mobile is a thin client. Nothing runs on your phone except the viewer. Your code, your shells, your AI agents — all of that stays on your desktop. The phone shows you what's happening and forwards your keystrokes back over an encrypted local-network connection.\n\nPairing is end-to-end encrypted. Your desktop generates a one-time keypair and shows a QR code; you scan it from the app. After that, all traffic between phone and desktop is sealed with that pair's keys.\n\nPRIVACY\n\n• No analytics SDK, no crash reporter, no third-party SDKs of any kind in the iOS app.\n• Pairing tokens and your preference list live in the app's own sandbox. The app's traffic to your desktop is end-to-end encrypted on your local network only.\n• The Orca desktop app collects anonymized usage metrics per the policy at https://www.onorca.dev/privacy. The mobile app itself does not.\n• Open source: every line is auditable at https://github.com/stablyai/orca.\n\nREQUIREMENTS\n\n• An iPhone (iOS 16+) on the same Wi-Fi as your desktop.\n• The Orca desktop app, free, from https://www.onorca.dev/ or https://github.com/stablyai/orca/releases.", + "trackCensoredName": "Orca IDE", + "trackViewUrl": "https://apps.apple.com/us/app/orca-ide/id6766130217?uo=4", + "contentAdvisoryRating": "4+", + "averageUserRating": 4.58621000000000034191316444776020944118499755859375, + "averageUserRatingForCurrentVersion": 4.58621000000000034191316444776020944118499755859375, + "languageCodesISO2A": ["EN"], + "fileSizeBytes": "41495552", + "formattedPrice": "Free", + "userRatingCountForCurrentVersion": 29, + "trackContentRating": "4+", + "sellerUrl": "https://www.onorca.dev/", + "minimumOsVersion": "15.1", + "userRatingCount": 29 + } + ] +} diff --git a/mobile/src/app-update/github-release-update-source.ts b/mobile/src/app-update/github-release-update-source.ts new file mode 100644 index 00000000000..c32c3475121 --- /dev/null +++ b/mobile/src/app-update/github-release-update-source.ts @@ -0,0 +1,77 @@ +import { z } from 'zod' +import { compareAppVersions } from '../../../src/shared/app-version' +import type { AppUpdateCheckResult, AppUpdateSource } from './app-update-source' +import { isNewerReleaseVersion } from './app-update-source' + +const REPO_API = 'https://api.github.com/repos/stablyai/orca' +// Why tag refs, not releases.atom or /releases?per_page=100: both are newest-first windows that a +// run of desktop releases fills, pushing the newest mobile release out and reading as "current". +// The release `prerelease` flag is not a filter: every mobile-android-v* release is published as one. +const TAG_PREFIX = 'mobile-android-v' +// Why: the release workflow pushes the tag before the APK build, so a failed build leaves a +// tag with no release; probe a few older candidates, bounded like the desktop's manifest probe. +const MAX_RELEASE_PROBES = 3 + +type Fetch = typeof fetch + +const tagRefsSchema = z.array(z.looseObject({ ref: z.string() })) +const installableReleaseSchema = z.looseObject({ + draft: z.literal(false), + html_url: z.string(), + assets: z.array(z.looseObject({ name: z.string() })) +}) + +/** Versions named by the tag refs GitHub returns for the mobile-android prefix. */ +export function parseMobileAndroidTagVersions(refs: unknown): string[] { + const prefix = `refs/tags/${TAG_PREFIX}` + return tagRefsSchema + .parse(refs) + .filter(({ ref }) => ref.startsWith(prefix)) + .map(({ ref }) => ref.slice(prefix.length)) +} + +/** The release page when the tag has a published release carrying an APK, else null. */ +export function installableReleaseUrl(release: unknown): string | null { + const parsed = installableReleaseSchema.safeParse(release) + return parsed.success && parsed.data.assets.some((asset) => asset.name.endsWith('.apk')) + ? parsed.data.html_url + : null +} + +export function createGithubReleaseUpdateSource(fetchImpl: Fetch): AppUpdateSource { + return { + async check(installedVersion, signal): Promise { + const refsReply = await fetchImpl(`${REPO_API}/git/matching-refs/tags/${TAG_PREFIX}`, { + signal + }) + if (!refsReply.ok) { + throw new Error(`tag refs HTTP ${refsReply.status}`) + } + const candidates = parseMobileAndroidTagVersions(await refsReply.json()) + .filter((version) => isNewerReleaseVersion(version, installedVersion)) + .sort((left, right) => compareAppVersions(right, left)) + .slice(0, MAX_RELEASE_PROBES) + for (const version of candidates) { + const releaseReply = await fetchImpl( + `${REPO_API}/releases/tags/${encodeURIComponent(`${TAG_PREFIX}${version}`)}`, + { signal } + ) + if (releaseReply.status === 404) { + continue + } + if (!releaseReply.ok) { + throw new Error(`release HTTP ${releaseReply.status}`) + } + const url = installableReleaseUrl(await releaseReply.json()) + if (url) { + return { kind: 'available', version, url } + } + } + return { kind: 'current' } + } + } +} + +export const githubReleaseUpdateSource = createGithubReleaseUpdateSource((input, init) => + fetch(input, init) +) diff --git a/mobile/src/components/mobile-agent-icon-assets.ts b/mobile/src/components/mobile-agent-icon-assets.ts index 8d0a6d474f7..53745ead77e 100644 --- a/mobile/src/components/mobile-agent-icon-assets.ts +++ b/mobile/src/components/mobile-agent-icon-assets.ts @@ -18,6 +18,7 @@ export const MOBILE_AGENT_ICON_ASSETS: Partial ({ + Pressable: 'Pressable', + StyleSheet: { create: (styles: T) => styles }, + Text: 'Text', + View: 'View' +})) +vi.mock('lucide-react-native', () => ({ ArrowUpFromLine: 'ArrowUpFromLine', X: 'X' })) + +describe('AppUpdateCard', () => { + let renderer: ReactTestRenderer | null = null + afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + }) + + function render() { + const onPress = vi.fn() + const onDismiss = vi.fn() + act(() => { + renderer = create(createElement(AppUpdateCard, { version: '0.0.51', onPress, onDismiss })) + }) + const root = renderer!.root + const texts = root + .findAll((node) => String(node.type) === 'Text') + .flatMap((node) => node.children.filter((child) => typeof child === 'string')) + return { root, texts, onPress, onDismiss } + } + + it('says which version is available and how to get it, with a monochrome glyph', () => { + const { root, texts } = render() + expect(texts).toEqual(['Orca 0.0.51 is available', 'Tap to update']) + expect(root.find((node) => String(node.type) === 'ArrowUpFromLine').props.color).toBe('#e0e0e0') + }) + + it('opens the update from the card body and dismisses from the X', () => { + const { root, onPress, onDismiss } = render() + const [body, dismiss] = root.findAll((node) => String(node.type) === 'Pressable') + expect(dismiss.props.accessibilityLabel).toBe('Dismiss Orca 0.0.51 update') + act(() => body.props.onPress()) + expect(onPress).toHaveBeenCalledTimes(1) + expect(onDismiss).not.toHaveBeenCalled() + act(() => dismiss.props.onPress()) + expect(onDismiss).toHaveBeenCalledTimes(1) + }) +}) diff --git a/mobile/src/home/AppUpdateCard.tsx b/mobile/src/home/AppUpdateCard.tsx new file mode 100644 index 00000000000..6713bcbfd12 --- /dev/null +++ b/mobile/src/home/AppUpdateCard.tsx @@ -0,0 +1,86 @@ +import { ArrowUpFromLine, X } from 'lucide-react-native' +import { Pressable, StyleSheet, Text, View } from 'react-native' +import { colors, radii, spacing } from '../theme/mobile-theme' + +/** Home's update notice, drawn in the host card's frame so it reads as part of that list. */ +export function AppUpdateCard(props: { + version: string + onPress: () => void + onDismiss: () => void +}) { + const title = `Orca ${props.version} is available` + return ( + + [styles.cardMain, pressed && styles.cardPressed]} + onPress={props.onPress} + > + + + + + + {title} + + + Tap to update + + + + [styles.dismissButton, pressed && styles.dismissPressed]} + onPress={props.onDismiss} + > + + + + ) +} + +// Same frame, icon well and spacing as MobileHostCard. +const styles = StyleSheet.create({ + card: { + flexDirection: 'row', + alignItems: 'center', + borderRadius: radii.card, + backgroundColor: colors.bgPanel, + borderWidth: 1, + borderColor: colors.borderSubtle, + overflow: 'hidden' + }, + cardMain: { + flex: 1, + minWidth: 0, + flexDirection: 'row', + alignItems: 'center', + paddingLeft: spacing.md, + paddingVertical: 12 + }, + cardPressed: { backgroundColor: colors.bgRaised }, + icon: { + width: 46, + height: 46, + borderRadius: 13, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.bgRaised, + marginRight: 14 + }, + main: { flex: 1, minWidth: 0, marginRight: spacing.sm }, + title: { color: colors.textPrimary, fontSize: 15, fontWeight: '600', lineHeight: 20 }, + subtitle: { color: colors.textSecondary, fontSize: 12, lineHeight: 16 }, + dismissButton: { + width: 40, + height: 40, + marginHorizontal: spacing.xs, + borderRadius: radii.row, + alignItems: 'center', + justifyContent: 'center' + }, + dismissPressed: { backgroundColor: colors.bgRaised } +}) diff --git a/mobile/src/home/MobileHomeAppUpdateCard.tsx b/mobile/src/home/MobileHomeAppUpdateCard.tsx new file mode 100644 index 00000000000..51bc41d8602 --- /dev/null +++ b/mobile/src/home/MobileHomeAppUpdateCard.tsx @@ -0,0 +1,27 @@ +import { StyleSheet, View } from 'react-native' +import { undismissedAppUpdate } from '../app-update/app-update-checker' +import { + appUpdateChecker, + openAppUpdate, + useAppUpdateState +} from '../app-update/app-update-runtime' +import { AppUpdateCard } from './AppUpdateCard' +import { spacing } from '../theme/mobile-theme' + +export function MobileHomeAppUpdateCard() { + const available = undismissedAppUpdate(useAppUpdateState()) + if (!available) { + return null + } + return ( + + openAppUpdate(available.url)} + onDismiss={() => appUpdateChecker.dismiss(available.version)} + /> + + ) +} + +const styles = StyleSheet.create({ slot: { marginBottom: spacing.lg } }) diff --git a/mobile/src/home/MobileHomeListHeader.tsx b/mobile/src/home/MobileHomeListHeader.tsx index 7e47c9ae9d6..f7da47c9733 100644 --- a/mobile/src/home/MobileHomeListHeader.tsx +++ b/mobile/src/home/MobileHomeListHeader.tsx @@ -1,6 +1,7 @@ import { StyleSheet, Text, View } from 'react-native' import type { HomeStatsSummary } from '../stats/home-stats-total' import { colors, spacing } from '../theme/mobile-theme' +import { MobileHomeAppUpdateCard } from './MobileHomeAppUpdateCard' function formatDuration(ms: number): string { const totalMinutes = Math.floor(ms / 60_000) @@ -36,6 +37,7 @@ export function MobileHomeListHeader({ stats }: { stats: HomeStatsSummary | null ) : null} + Desktops ) diff --git a/mobile/src/session/MobileNativeChatMessage.test.ts b/mobile/src/session/MobileNativeChatMessage.test.ts index 67e90132866..935fe2ef0f3 100644 --- a/mobile/src/session/MobileNativeChatMessage.test.ts +++ b/mobile/src/session/MobileNativeChatMessage.test.ts @@ -285,3 +285,74 @@ describe('MobileNativeChatMessage', () => { }) }) }) + +describe("MobileNativeChatMessage — a subagent's row speaks as that subagent", () => { + let renderer: ReactTestRenderer | null = null + + afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + }) + + function renderAgentRow(agentId: string | undefined, subagentLabel?: string): ReactTestRenderer { + const message: NativeChatMessage = { + id: 'a1', + role: 'assistant', + blocks: [{ type: 'text', text: 'The PR is CLEAN.' }], + timestamp: null, + source: 'transcript', + ...(agentId === undefined ? {} : { agentId }) + } + act(() => { + renderer = create(createElement(MobileNativeChatMessage, { message, subagentLabel })) + }) + return renderer! + } + + const captions = (tree: ReactTestRenderer): ReactTestInstance[] => + tree.root + .findAllByType('Text' as never) + .filter((node) => typeof node.props.accessibilityLabel === 'string') + + it('names the subagent that wrote the row', () => { + const [caption] = captions(renderAgentRow('task-1', 'review the PR')) + expect(caption?.props.accessibilityLabel).toBe('Written by subagent review the PR') + expect(caption?.children.join('')).toBe('review the PR') + }) + + it('still marks the row as a subagent when no loaded roster names it', () => { + const [caption] = captions(renderAgentRow('task-9')) + expect(caption?.children.join('')).toBe('Subagent') + }) + + it("adds nothing to the session's own row", () => { + expect(captions(renderAgentRow(undefined, 'review the PR'))).toEqual([]) + }) + + it('names no one on a settled row whose only content is hidden, and names the live one', () => { + const toolOnly: NativeChatMessage = { + id: 'a2', + role: 'assistant', + blocks: [{ type: 'tool-call', name: 'Grep', input: {}, state: 'completed' }], + timestamp: null, + source: 'transcript', + agentId: 'task-1' + } + const renderToolOnly = (activeTurnIsWorking: boolean): ReactTestRenderer => { + act(() => { + renderer = create( + createElement(MobileNativeChatMessage, { + message: toolOnly, + subagentLabel: 'review the PR', + structuredActivityUi: true, + activeTurnIsWorking + }) + ) + }) + return renderer! + } + expect(captions(renderToolOnly(false))).toEqual([]) + act(() => renderer?.unmount()) + expect(captions(renderToolOnly(true))).toHaveLength(1) + }) +}) diff --git a/mobile/src/session/MobileNativeChatMessage.tsx b/mobile/src/session/MobileNativeChatMessage.tsx index a290a999161..08feb9bc7ce 100644 --- a/mobile/src/session/MobileNativeChatMessage.tsx +++ b/mobile/src/session/MobileNativeChatMessage.tsx @@ -4,6 +4,8 @@ import { Image, Text as NativeText, View } from 'react-native' import { splitNativeChatBlocks } from '../../../src/shared/native-chat-tool-fold' import { selectActiveToolCall } from '../../../src/shared/native-chat-tool-activity' import { isImageRefBlock, isTextBlock } from '../../../src/shared/native-chat-types' +import { agentJournalItemSubagentId } from '../../../src/shared/agent-session-journal-producer' +import { NATIVE_CHAT_SUBAGENT_ATTRIBUTION_COPY } from '../../../src/shared/native-chat-subagent-attribution' import type { NativeChatBlock, NativeChatMessage } from '../../../src/shared/native-chat-types' import { MobileMarkdown } from '../components/MobileMarkdown' import { MobileNativeChatTurnStatus } from './MobileNativeChatTurnStatus' @@ -75,7 +77,8 @@ function MobileNativeChatMessageImpl({ turnKey, onToggleTurn, activeTurnIsWorking, - structuredActivityUi = false + structuredActivityUi = false, + subagentLabel }: { message: NativeChatMessage toolsExpanded?: boolean @@ -94,6 +97,8 @@ function MobileNativeChatMessageImpl({ activeTurnIsWorking?: boolean /** Structured lane only: live tool progress plus the turn-status disclosure. */ structuredActivityUi?: boolean + /** The roster's name for the subagent that wrote this row, when one names it. */ + subagentLabel?: string }): React.JSX.Element { const isUser = message.role === 'user' const isReasoning = message.role === 'reasoning' @@ -115,13 +120,40 @@ function MobileNativeChatMessageImpl({ !turnExpanded && !toolsExpanded const showToolRun = tools.length > 0 && !settledToolsHidden + // A subagent's row sits where it happened but speaks as that subagent. A row + // whose only content is hidden behind its settled turn names no one. + const subagentName = + isUser || agentJournalItemSubagentId(message) === null || (prose.length === 0 && !showToolRun) + ? null + : (subagentLabel ?? NATIVE_CHAT_SUBAGENT_ATTRIBUTION_COPY.unnamed) return ( <> + {subagentName !== null ? ( + + {subagentName} + + ) : null} {prose.map((block, index) => ( void @@ -147,6 +153,7 @@ export function MobileNativeChatView({ turnIndicator = null, workingStartedAt, settledTurns, + activeTurnOpenedBy = null, onStop, streaming, hasMore, @@ -209,6 +216,7 @@ export function MobileNativeChatView({ }), [messages, folded, streaming, pending, imagePreviewsByMessageId] ) + const subagentLabels = useMemo(() => nativeChatSubagentLabels(messages), [messages]) const { listRef, showJumpToTail, @@ -265,6 +273,7 @@ export function MobileNativeChatView({ isWorking: agentWorking === true, workingStartedAt, settledTurns, + activeTurnOpenedBy, thinking: turnIndicator?.thinking === true, activityText: turnIndicator?.activityText ?? null, scopeKey: sendSurfaceId @@ -281,10 +290,11 @@ export function MobileNativeChatView({ onOpenFile={onOpenFile} structuredActivityUi={structuredActivityUi} onToggleTurn={turns.onToggleTurn} + subagentLabel={nativeChatSubagentLabel(subagentLabels, item)} {...turns.resolveRow(index, item)} /> ), - [toolsExpanded, fontScale, onOpenFile, structuredActivityUi, turns] + [toolsExpanded, fontScale, onOpenFile, structuredActivityUi, subagentLabels, turns] ) const emptyState = mobileNativeChatEmptyState(status, agent ?? null, error) diff --git a/mobile/src/session/mobile-native-chat-controller-contract.ts b/mobile/src/session/mobile-native-chat-controller-contract.ts index 78c5ebeb1cf..686dac407e7 100644 --- a/mobile/src/session/mobile-native-chat-controller-contract.ts +++ b/mobile/src/session/mobile-native-chat-controller-contract.ts @@ -37,6 +37,8 @@ export type MobileNativeChatController = { /** Structured lane: host-recorded turn timing for the per-turn status rows. */ nativeChatWorkingStartedAt: number | null nativeChatSettledTurns: NativeChatSettledTurns | null + /** Structured lane: the user message the host says opened the running turn. */ + nativeChatActiveTurnOpenedBy: string | null nativeChatCanStop: boolean nativeChatStreamingText?: string /** Agent mid-turn, regardless of whether chat is the visible view. */ diff --git a/mobile/src/session/mobile-native-chat-message-styles.ts b/mobile/src/session/mobile-native-chat-message-styles.ts index 51ff9ab1f36..88e97018521 100644 --- a/mobile/src/session/mobile-native-chat-message-styles.ts +++ b/mobile/src/session/mobile-native-chat-message-styles.ts @@ -32,6 +32,17 @@ export const styles = StyleSheet.create({ reasoning: { opacity: 0.7 }, + // A subagent's row is an aside to the conversation, set off the way desktop sets it off. + subagent: { + borderLeftWidth: 2, + borderLeftColor: colors.borderSubtle, + paddingLeft: spacing.md + }, + subagentCaption: { + color: colors.textMuted, + fontFamily: typography.monoFamily, + fontSize: MONO_SIZE + }, toolRun: { marginTop: spacing.xs }, diff --git a/mobile/src/session/use-mobile-native-chat-controller.ts b/mobile/src/session/use-mobile-native-chat-controller.ts index dfefcf47627..7a1fa7c468e 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.ts @@ -288,6 +288,9 @@ export function useMobileNativeChatController(args: { nativeChatTurnIndicator: activeChatStructured ? structuredNativeChat.turnIndicator : null, nativeChatWorkingStartedAt: activeChatStructured ? structuredNativeChat.workingStartedAt : null, nativeChatSettledTurns: activeChatStructured ? structuredNativeChat.settledTurns : null, + nativeChatActiveTurnOpenedBy: activeChatStructured + ? structuredNativeChat.activeTurnOpenedBy + : null, nativeChatCanStop: activeChatStructured ? structuredNativeChat.turnId !== null : nativeChatAgentWorking, diff --git a/mobile/src/session/use-mobile-native-chat-turn-disclosure.test.tsx b/mobile/src/session/use-mobile-native-chat-turn-disclosure.test.tsx index 3546a568e73..1757a8ce65b 100644 --- a/mobile/src/session/use-mobile-native-chat-turn-disclosure.test.tsx +++ b/mobile/src/session/use-mobile-native-chat-turn-disclosure.test.tsx @@ -20,12 +20,16 @@ function Harness({ enabled, isWorking = true, settledTurns, + workingStartedAt, + activeTurnOpenedBy, scopeKey = 'host\0worktree\0tab-a' }: { messages: readonly NativeChatMessage[] enabled: boolean isWorking?: boolean settledTurns?: NativeChatSettledTurns + workingStartedAt?: number | null + activeTurnOpenedBy?: string | null scopeKey?: string }): React.JSX.Element { const disclosure = useMobileNativeChatTurnDisclosure({ @@ -33,6 +37,8 @@ function Harness({ enabled, isWorking, settledTurns, + workingStartedAt, + activeTurnOpenedBy, scopeKey }) return createElement('result', { disclosure }) @@ -202,6 +208,62 @@ describe('useMobileNativeChatTurnDisclosure', () => { } }) + it('keeps the live bar under the prompt that opened the running turn, not a mid-turn send', () => { + vi.useFakeTimers() + try { + vi.setSystemTime(10_000) + const tool: NativeChatMessage = { + id: 'tool-a', + role: 'assistant', + blocks: [ + { type: 'tool-call', name: 'Bash', input: { command: 'sleep 15' }, state: 'running' } + ], + timestamp: null, + source: 'transcript' + } + const messages = [userMessage('A'), tool, userMessage('B')] + const rows = () => { + const disclosure = renderer!.root.findByType('result').props.disclosure + return messages.map((message, index) => disclosure.resolveRow(index, message)) + } + // B was sent while A's turn runs; the host still names A as the running turn's opener. + act(() => { + renderer = create( + createElement(Harness, { + messages, + enabled: true, + workingStartedAt: 5_000, + activeTurnOpenedBy: 'A' + }) + ) + }) + let [rowA, rowTool, rowB] = rows() + expect(rowA.turnStatus).toEqual({ startedAt: 5_000, thinking: false, workedSeconds: null }) + expect(rowB.turnStatus).toBeNull() + // Row liveness still follows the transcript's grouping under the newest prompt. + expect(rowTool.activeTurnIsWorking).toBe(false) + expect(rowB.activeTurnIsWorking).toBe(true) + + // B's own turn opens: A takes the host's settled duration, B counts from A's end. + act(() => { + renderer?.update( + createElement(Harness, { + messages, + enabled: true, + workingStartedAt: 22_000, + settledTurns: new Map([['A', { startedAt: 5_000, workedSeconds: 17 }]]), + activeTurnOpenedBy: 'B' + }) + ) + }) + ;[rowA, , rowB] = rows() + expect(rowA.turnStatus).toEqual({ startedAt: 5_000, thinking: false, workedSeconds: 17 }) + expect(rowB.turnStatus).toEqual({ startedAt: 22_000, thinking: false, workedSeconds: null }) + } finally { + vi.useRealTimers() + } + }) + it('keeps at most the latest 128 turns expanded', () => { vi.useFakeTimers() try { diff --git a/mobile/src/session/use-mobile-native-chat-turn-disclosure.ts b/mobile/src/session/use-mobile-native-chat-turn-disclosure.ts index 4e236d648be..6cbb31c723a 100644 --- a/mobile/src/session/use-mobile-native-chat-turn-disclosure.ts +++ b/mobile/src/session/use-mobile-native-chat-turn-disclosure.ts @@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from 'react' import type { NativeChatMessage } from '../../../src/shared/native-chat-types' import type { NativeChatSettledTurns } from '../../../src/shared/native-chat-turn-status' import { - MOBILE_UNANCHORED_TURN_KEY, useMobileNativeChatTurnStatus, type NativeChatTurnStatus } from './use-mobile-native-chat-turn-status' @@ -28,6 +27,7 @@ export function useMobileNativeChatTurnDisclosure({ isWorking, workingStartedAt, settledTurns, + activeTurnOpenedBy = null, thinking = false, activityText = null, scopeKey @@ -38,6 +38,8 @@ export function useMobileNativeChatTurnDisclosure({ workingStartedAt?: number | null /** Host-recorded durations; they outrank whatever this client observed. */ settledTurns?: NativeChatSettledTurns | null + /** The user message the host says opened the running turn; absent, the latest one. */ + activeTurnOpenedBy?: string | null /** Whether the turn is reasoning right now, derived from its journal content. */ thinking?: boolean /** What the provider says the live turn is doing; outranks the other labels. */ @@ -57,6 +59,7 @@ export function useMobileNativeChatTurnDisclosure({ isWorking, workingStartedAt, settledTurns, + activeTurnOpenedBy, thinking, scopeKey }) @@ -100,6 +103,8 @@ export function useMobileNativeChatTurnDisclosure({ }, [enabled, messages]) const { active, activeTurnKey, completedByTurn } = turnStatuses + // Liveness follows the transcript's own grouping (the latest prompt's rows), not the bar's owner. + const latestTurnKey = turnKeys.at(-1) const activeActivityText = enabled && isWorking ? (activityText ?? null) : null const resolveRow = useCallback( (index: number, message: NativeChatMessage): MobileNativeChatTurnRow => { @@ -120,14 +125,19 @@ export function useMobileNativeChatTurnDisclosure({ // writing a ref during render, which react-freeze can discard. turnKey: turnKey && turnStatus?.workedSeconds != null ? turnKey : undefined, // With no user boundary at all, the session's working state stays authoritative. - activeTurnIsWorking: - enabled && - isWorking && - (turnKey === activeTurnKey || - (turnKey === undefined && activeTurnKey === MOBILE_UNANCHORED_TURN_KEY)) + activeTurnIsWorking: enabled && isWorking && turnKey === latestTurnKey } }, - [turnKeys, enabled, activeTurnKey, active, completedByTurn, expandedTurnIds, isWorking] + [ + turnKeys, + enabled, + activeTurnKey, + latestTurnKey, + active, + completedByTurn, + expandedTurnIds, + isWorking + ] ) return { diff --git a/mobile/src/session/use-mobile-native-chat-turn-status.ts b/mobile/src/session/use-mobile-native-chat-turn-status.ts index f7cbb2dd887..3dac27e1b25 100644 --- a/mobile/src/session/use-mobile-native-chat-turn-status.ts +++ b/mobile/src/session/use-mobile-native-chat-turn-status.ts @@ -1,7 +1,9 @@ import { useEffect, useMemo, useRef, useState } from 'react' import type { NativeChatMessage } from '../../../src/shared/native-chat-types' import { + NATIVE_CHAT_UNANCHORED_TURN_KEY, reduceNativeChatTurnTiming, + selectNativeChatActiveTurnKey, selectNativeChatTurnStatuses, type NativeChatSettledTurns, type NativeChatTurnStatus, @@ -10,7 +12,6 @@ import { export type { NativeChatTurnStatus } -export const MOBILE_UNANCHORED_TURN_KEY = '__unanchored__' const EMPTY_TURN_TIMING_BY_TURN: NativeChatTurnTimingByTurn = Object.freeze({}) type ScopedTurnTiming = { @@ -26,6 +27,7 @@ export function useMobileNativeChatTurnStatus({ isWorking, workingStartedAt, settledTurns, + activeTurnOpenedBy = null, thinking = false, scopeKey }: { @@ -35,6 +37,8 @@ export function useMobileNativeChatTurnStatus({ workingStartedAt?: number | null /** Host-recorded durations; they outrank whatever this client observed. */ settledTurns?: NativeChatSettledTurns | null + /** The user message the host says opened the running turn; absent, the latest one. */ + activeTurnOpenedBy?: string | null /** Whether the turn is reasoning right now, derived from its journal content. */ thinking?: boolean /** Host/worktree/tab identity. Timings never carry across chat surfaces. */ @@ -44,11 +48,9 @@ export function useMobileNativeChatTurnStatus({ completedByTurn: Readonly> activeTurnKey: string } { - const latestUserIndex = enabled - ? messages.findLastIndex((message) => message.role === 'user') - : -1 - const latestUserId = latestUserIndex !== -1 ? (messages[latestUserIndex]?.id ?? null) : null - const activeTurnKey = latestUserId ?? MOBILE_UNANCHORED_TURN_KEY + const activeTurnKey = enabled + ? selectNativeChatActiveTurnKey(messages, activeTurnOpenedBy) + : NATIVE_CHAT_UNANCHORED_TURN_KEY const [scopedTiming, setScopedTiming] = useState(() => ({ scopeKey, timingByTurn: {} diff --git a/mobile/src/session/use-mobile-structured-agent-turn-timing.ts b/mobile/src/session/use-mobile-structured-agent-turn-timing.ts index 811320510a2..3559f6925ed 100644 --- a/mobile/src/session/use-mobile-structured-agent-turn-timing.ts +++ b/mobile/src/session/use-mobile-structured-agent-turn-timing.ts @@ -5,18 +5,16 @@ import type { } from '../../../src/shared/agent-session-journal-types' import type { NativeChatSettledTurns } from '../../../src/shared/native-chat-turn-status' import type { StructuredAgentHostClock } from '../../../src/shared/structured-agent-session-reducer' -import { - selectStructuredAgentRunningTurnTiming, - selectStructuredAgentSettledTurns -} from '../../../src/shared/structured-agent-session-turn-timing' +import { selectStructuredAgentTurnBars } from '../../../src/shared/structured-agent-session-turn-timing' import { stepStructuredAgentTurnClock, type StructuredAgentTurnClockLatch } from '../../../src/shared/structured-agent-turn-clock-anchor' /** Host-recorded turn timing for the structured lane: settled durations straight - * off the journal, and a skew-free start for the live counter whose host-to-local - * conversion is latched once per turn. */ + * off the journal, the user message that owns the running turn's bar, and a + * skew-free start for the live counter whose host-to-local conversion is latched + * once per turn. */ export function useMobileStructuredAgentTurnTiming( { items, @@ -28,16 +26,16 @@ export function useMobileStructuredAgentTurnTiming( hostClock?: StructuredAgentHostClock | null }, turnId: string | null -): { settledTurns: NativeChatSettledTurns; workingStartedAt: number | null } { - const settledTurns = useMemo( - () => selectStructuredAgentSettledTurns(items, submissions), - [items, submissions] +): { + settledTurns: NativeChatSettledTurns + workingStartedAt: number | null + activeTurnOpenedBy: string | null +} { + const { settledTurns, runningTiming, activeTurnOpenedBy } = useMemo( + () => selectStructuredAgentTurnBars(items, submissions, turnId), + [items, submissions, turnId] ) const [latch, setLatch] = useState(null) - const runningTiming = useMemo( - () => (turnId === null ? null : selectStructuredAgentRunningTurnTiming(items, turnId)), - [items, turnId] - ) // Stamp during render (React's derive-from-props pattern) so the first paint of // a new turn already counts from the right instant. const step = stepStructuredAgentTurnClock({ @@ -50,5 +48,5 @@ export function useMobileStructuredAgentTurnTiming( if (step.latch !== latch) { setLatch(step.latch) } - return { settledTurns, workingStartedAt: step.workingStartedAt } + return { settledTurns, workingStartedAt: step.workingStartedAt, activeTurnOpenedBy } } diff --git a/mobile/src/settings/app-update-settings-rows.test.tsx b/mobile/src/settings/app-update-settings-rows.test.tsx new file mode 100644 index 00000000000..77177bf7d63 --- /dev/null +++ b/mobile/src/settings/app-update-settings-rows.test.tsx @@ -0,0 +1,90 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { AppUpdateSettingsRows, type AppUpdateCheckRowStatus } from './app-update-settings-rows' + +vi.mock('react-native', () => ({ + Pressable: 'Pressable', + StyleSheet: { create: (styles: T) => styles, hairlineWidth: 1 }, + Text: 'Text', + View: 'View' +})) + +const NOW = Date.UTC(2026, 8, 28, 12) +const HOUR = 60 * 60 * 1000 + +describe('AppUpdateSettingsRows', () => { + let renderer: ReactTestRenderer | null = null + afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + }) + + function render(opts: { + available?: { version: string; url: string } | null + lastCheckedAt?: number | null + checkStatus?: AppUpdateCheckRowStatus + }) { + const onUpdate = vi.fn() + const onCheck = vi.fn() + act(() => { + renderer = create( + createElement(AppUpdateSettingsRows, { + installedVersion: '0.0.48', + available: opts.available ?? null, + lastCheckedAt: opts.lastCheckedAt ?? null, + now: NOW, + checkStatus: opts.checkStatus ?? 'idle', + onUpdate, + onCheck + }) + ) + }) + const root = renderer!.root + const texts = root + .findAll((node) => String(node.type) === 'Text') + .map((node) => node.children.filter((child) => typeof child === 'string').join('')) + return { + root, + texts, + onUpdate, + onCheck, + pressables: root.findAll((node) => String(node.type) === 'Pressable') + } + } + + it('offers the known update as a tappable row with an Update action', () => { + const { texts, pressables, onUpdate } = render({ + available: { version: '0.0.51', url: 'https://example.test' }, + lastCheckedAt: NOW - 2 * HOUR + }) + expect(texts).toEqual([ + 'Update to Orca 0.0.51', + 'Update', + 'Check for updates', + 'Last checked 2h ago' + ]) + act(() => pressables[0].props.onPress()) + expect(onUpdate).toHaveBeenCalledTimes(1) + }) + + it('shows a plain, untappable version row when current, and Never before a first success', () => { + const { texts, pressables } = render({}) + expect(texts).toEqual(['Version 0.0.48', 'Check for updates', 'Never']) + expect(pressables).toHaveLength(1) + expect(pressables[0].props.accessibilityLabel).toBe('Check for updates, Never') + }) + + it('runs the check from its row and shows the transient outcome in place', () => { + const idle = render({ lastCheckedAt: NOW - 30_000 }) + expect(idle.texts.at(-1)).toBe('Last checked just now') + act(() => idle.pressables[0].props.onPress()) + expect(idle.onCheck).toHaveBeenCalledTimes(1) + act(() => renderer?.unmount()) + expect(render({ checkStatus: 'checking' }).pressables[0].props.disabled).toBe(true) + act(() => renderer?.unmount()) + expect(render({ checkStatus: 'up-to-date' }).texts.at(-1)).toBe('Up to date') + act(() => renderer?.unmount()) + expect(render({ checkStatus: 'failed' }).texts.at(-1)).toBe("Couldn't check") + }) +}) diff --git a/mobile/src/settings/app-update-settings-rows.tsx b/mobile/src/settings/app-update-settings-rows.tsx new file mode 100644 index 00000000000..b605a89105b --- /dev/null +++ b/mobile/src/settings/app-update-settings-rows.tsx @@ -0,0 +1,100 @@ +import { Pressable, StyleSheet, Text, View } from 'react-native' +import type { KnownAppUpdate } from '../storage/app-update-preferences' +import { colors, spacing, typography } from '../theme/mobile-theme' +import { formatTimeAgo } from '../worktree/agent-row-display' + +export type AppUpdateCheckRowStatus = 'idle' | 'checking' | 'up-to-date' | 'failed' + +function checkRowValue(status: AppUpdateCheckRowStatus, lastCheckedAt: number | null, now: number) { + if (status === 'checking') { + return 'Checking…' + } + if (status === 'up-to-date') { + return 'Up to date' + } + if (status === 'failed') { + return "Couldn't check" + } + if (lastCheckedAt === null) { + return 'Never' + } + const ago = formatTimeAgo(lastCheckedAt, now) + return `Last checked ${ago === 'just now' ? ago : `${ago} ago`}` +} + +/** Settings' version row and manual check; the update row stays after the home card is dismissed. */ +export function AppUpdateSettingsRows(props: { + installedVersion: string | null + available: KnownAppUpdate | null + lastCheckedAt: number | null + now: number + checkStatus: AppUpdateCheckRowStatus + onUpdate: (url: string) => void + onCheck: () => void +}) { + const checkValue = checkRowValue(props.checkStatus, props.lastCheckedAt, props.now) + const { available, installedVersion, onUpdate } = props + const versionRow = available ? ( + [styles.row, pressed && styles.rowPressed]} + onPress={() => onUpdate(available.url)} + > + Update to Orca {available.version} + Update + + ) : installedVersion ? ( + + Version {installedVersion} + + ) : null + return ( + + {versionRow} + {versionRow && } + [styles.row, pressed && styles.rowPressed]} + onPress={props.onCheck} + > + Check for updates + {checkValue} + + + ) +} + +// Same panel, rows and separator as MobileSettingsSection. +const styles = StyleSheet.create({ + section: { + backgroundColor: colors.bgPanel, + borderRadius: 12, + overflow: 'hidden', + marginTop: spacing.md + }, + row: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm + 2, + paddingVertical: spacing.md, + paddingHorizontal: spacing.md + 2 + }, + rowPressed: { backgroundColor: colors.bgRaised }, + rowLabel: { + flex: 1, + fontSize: typography.bodySize, + fontWeight: '500', + color: colors.textPrimary + }, + rowValue: { fontSize: typography.metaSize, color: colors.textSecondary }, + actionValue: { fontSize: typography.bodySize, fontWeight: '600', color: colors.textPrimary }, + separator: { + height: StyleSheet.hairlineWidth, + backgroundColor: colors.borderSubtle, + marginHorizontal: spacing.md + } +}) diff --git a/mobile/src/settings/settings-app-update-section.tsx b/mobile/src/settings/settings-app-update-section.tsx new file mode 100644 index 00000000000..4edc93d95a9 --- /dev/null +++ b/mobile/src/settings/settings-app-update-section.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react' +import { + appUpdateChecker, + installedAppVersion, + openAppUpdate, + useAppUpdateState +} from '../app-update/app-update-runtime' +import { AppUpdateSettingsRows, type AppUpdateCheckRowStatus } from './app-update-settings-rows' + +const CHECK_RESULT_VISIBLE_MS = 3000 +const RELATIVE_TIME_TICK_MS = 60_000 + +export function SettingsAppUpdateSection() { + const state = useAppUpdateState() + const [result, setResult] = useState<'up-to-date' | 'failed' | null>(null) + // A check newer than this reads as "just now", so the tick only has to age the label. + const [now, setNow] = useState(() => Date.now()) + + useEffect(() => { + const tick = setInterval(() => setNow(Date.now()), RELATIVE_TIME_TICK_MS) + return () => clearInterval(tick) + }, []) + + useEffect(() => { + if (result === null) { + return + } + const hide = setTimeout(() => setResult(null), CHECK_RESULT_VISIBLE_MS) + return () => clearTimeout(hide) + }, [result]) + + const runCheck = () => { + setResult(null) + void appUpdateChecker.checkNow().then((outcome) => { + // An available update shows in the row above, so only the other outcomes need words here. + setResult(outcome === 'available' ? null : outcome) + }) + } + + const checkStatus: AppUpdateCheckRowStatus = state.checking ? 'checking' : (result ?? 'idle') + return ( + + ) +} diff --git a/mobile/src/storage/app-update-preferences.test.ts b/mobile/src/storage/app-update-preferences.test.ts new file mode 100644 index 00000000000..dbd7ba4215d --- /dev/null +++ b/mobile/src/storage/app-update-preferences.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it, vi } from 'vitest' +import { loadAppUpdatePreferences } from './app-update-preferences' + +const stored = new Map() +vi.mock('@react-native-async-storage/async-storage', () => ({ + default: { + multiGet: async (keys: string[]) => keys.map((key) => [key, stored.get(key) ?? null]) + } +})) + +describe('loadAppUpdatePreferences', () => { + it('drops only a corrupt update record, keeping the check time and dismissal beside it', async () => { + stored.set('orca:appUpdate:lastCheckedAt', '1700000000000') + stored.set('orca:appUpdate:latest', '{not json') + stored.set('orca:appUpdate:dismissedVersion', '0.0.51') + await expect(loadAppUpdatePreferences()).resolves.toEqual({ + lastCheckedAt: 1700000000000, + latest: null, + dismissedVersion: '0.0.51' + }) + }) + + it('reads a well-formed update record', async () => { + stored.set( + 'orca:appUpdate:latest', + JSON.stringify({ version: '0.0.52', url: 'https://x.test' }) + ) + await expect(loadAppUpdatePreferences()).resolves.toMatchObject({ + latest: { version: '0.0.52', url: 'https://x.test' } + }) + }) +}) diff --git a/mobile/src/storage/app-update-preferences.ts b/mobile/src/storage/app-update-preferences.ts new file mode 100644 index 00000000000..51e199a68b1 --- /dev/null +++ b/mobile/src/storage/app-update-preferences.ts @@ -0,0 +1,64 @@ +import AsyncStorage from '@react-native-async-storage/async-storage' +import { z } from 'zod' + +const LAST_CHECKED_AT_KEY = 'orca:appUpdate:lastCheckedAt' +const LATEST_KEY = 'orca:appUpdate:latest' +const DISMISSED_VERSION_KEY = 'orca:appUpdate:dismissedVersion' + +export type KnownAppUpdate = { readonly version: string; readonly url: string } + +export type AppUpdatePreferences = { + /** Last successful check; failures never move it. */ + readonly lastCheckedAt: number | null + /** Newest installable release that check saw, or null when it saw none. */ + readonly latest: KnownAppUpdate | null + readonly dismissedVersion: string | null +} + +export const EMPTY_APP_UPDATE_PREFERENCES: AppUpdatePreferences = { + lastCheckedAt: null, + latest: null, + dismissedVersion: null +} + +const knownAppUpdateSchema = z.object({ version: z.string(), url: z.string() }) + +// A corrupt record loses only itself, never the check time or the dismissal stored beside it. +function parseLatest(raw: string | null): KnownAppUpdate | null { + try { + const parsed = raw ? knownAppUpdateSchema.safeParse(JSON.parse(raw)) : null + return parsed?.success ? parsed.data : null + } catch { + return null + } +} + +export async function loadAppUpdatePreferences(): Promise { + try { + const [[, checkedAt], [, latest], [, dismissed]] = await AsyncStorage.multiGet([ + LAST_CHECKED_AT_KEY, + LATEST_KEY, + DISMISSED_VERSION_KEY + ]) + const lastCheckedAt = checkedAt === null ? null : Number(checkedAt) + return { + lastCheckedAt: + lastCheckedAt !== null && Number.isFinite(lastCheckedAt) ? lastCheckedAt : null, + latest: parseLatest(latest), + dismissedVersion: dismissed + } + } catch { + return EMPTY_APP_UPDATE_PREFERENCES + } +} + +export async function saveAppUpdateCheck(checkedAt: number, latest: KnownAppUpdate | null) { + await AsyncStorage.multiSet([ + [LAST_CHECKED_AT_KEY, String(checkedAt)], + [LATEST_KEY, latest ? JSON.stringify(latest) : ''] + ]) +} + +export async function saveDismissedAppUpdateVersion(version: string): Promise { + await AsyncStorage.setItem(DISMISSED_VERSION_KEY, version) +} diff --git a/mobile/src/tasks/mobile-tui-agents.ts b/mobile/src/tasks/mobile-tui-agents.ts index e1dad2269d6..3b601ec2f68 100644 --- a/mobile/src/tasks/mobile-tui-agents.ts +++ b/mobile/src/tasks/mobile-tui-agents.ts @@ -15,6 +15,7 @@ export const MOBILE_TUI_AGENT_LABELS: Record = TUI_AGENT_DISPL export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial> = { openclaude: 'openclaude.gitlawb.com', + codebuddy: 'codebuddy.ai', grok: 'x.ai', copilot: 'github.com', opencode: 'opencode.ai', diff --git a/mobile/src/test-support/rpc-recording/README.md b/mobile/src/test-support/rpc-recording/README.md index 8f8bbc89fc6..afe33f98bda 100644 --- a/mobile/src/test-support/rpc-recording/README.md +++ b/mobile/src/test-support/rpc-recording/README.md @@ -529,9 +529,15 @@ a tree that does not produce them, which is the one claim this header exists to derived scenarios rather than the manifest, so a repin alone never moves it. 5. Commit the repin and the refresh together, and state the cause. -After a squash-merge the pinned sha is unreachable from main, so the next recording on main repins -to main's tip in a follow-up — the same two-step #20563 and #20895 used. A reviewer checking an -in-flight branch resolves the pin against the branch, where it is a real commit. +A squash-merge leaves the pinned sha out of main's history, and no follow-up repin is needed: the +pin guard asks GitHub which pull requests contain the pinned commit +(`GET /repos/{owner}/{repo}/commits/{sha}/pulls`), fetches each one's `refs/pull//head`, which +GitHub keeps after the branch is deleted, and passes only when git finds the pin in one of those +heads. The squash title plays no part, so editing it at merge time is safe. Run `pnpm --dir mobile +exec tsx scripts/rpc-recording-pin-guard.mts reachable` to fetch a pin your clone does not have +before checking it out. It works without a token, and sends `GITHUB_TOKEN` or `GH_TOKEN` when one +is set, which lifts GitHub's unauthenticated rate limit. A reviewer checking an in-flight branch +resolves the pin against the branch, where it is a real commit. Editing the recorder engine on a migration branch is the awkward case: `recorderSha256` moves, so every golden needs rewriting, but the product tree no longer matches `baseline`, and bumping diff --git a/mobile/src/transport/rpc-client-session-tabs-stream.ts b/mobile/src/transport/rpc-client-session-tabs-stream.ts new file mode 100644 index 00000000000..f98a7586921 --- /dev/null +++ b/mobile/src/transport/rpc-client-session-tabs-stream.ts @@ -0,0 +1,34 @@ +import { isSnapshotResult } from './rpc-subscription-result-shapes' + +type SessionTabsStreamState = { + method: string + sent?: boolean + receivedSnapshot?: boolean + cancelled?: boolean +} + +/** Older hosts register a tabs stream only as they emit the first snapshot, so an earlier unsubscribe + * finds nothing there. Newer hosts register on arrival and still send that snapshot, so the hold only delays. */ +function awaitsRegistration(stream: SessionTabsStreamState): boolean { + return ( + stream.method === 'session.tabs.subscribe' && stream.sent === true && !stream.receivedSnapshot + ) +} + +/** Cancels a stream still awaiting its first snapshot; that snapshot then triggers the unsubscribe. */ +export function holdUnsubscribe(stream: SessionTabsStreamState): boolean { + if (!awaitsRegistration(stream)) { + return false + } + stream.cancelled = true + return true +} + +/** Returns true when `result` is the tabs stream's snapshot, i.e. the host has registered it. */ +export function recordSnapshot(stream: SessionTabsStreamState, result: unknown): boolean { + if (stream.method !== 'session.tabs.subscribe' || !isSnapshotResult(result)) { + return false + } + stream.receivedSnapshot = true + return true +} diff --git a/mobile/src/transport/rpc-client-stream-registry.test.ts b/mobile/src/transport/rpc-client-stream-registry.test.ts index 6afcd8a7ef3..ce210333618 100644 --- a/mobile/src/transport/rpc-client-stream-registry.test.ts +++ b/mobile/src/transport/rpc-client-stream-registry.test.ts @@ -79,6 +79,104 @@ describe('RpcClientStreamRegistry', () => { ]) }) + it('unsubscribes a session tabs stream by its own request id', () => { + const { registry, sent } = createRegistry() + const disposeOlder = registry.subscribe( + 'session.tabs.subscribe', + { worktree: 'wt-1' }, + () => {} + ) + registry.subscribe('session.tabs.subscribe', { worktree: 'wt-1' }, () => {}) + const [older, newer] = sent + registry.handleResponse(streamingResponse(older!.id, { type: 'snapshot', tabs: [] })) + + disposeOlder() + + // Without the request id the host sweeps every stream for the worktree, including the newer one. + expect(sent[2]).toMatchObject({ + method: 'session.tabs.unsubscribe', + params: { worktree: 'wt-1', subscriptionId: older!.id } + }) + expect(newer!.id).not.toBe(older!.id) + expect(sent).toHaveLength(3) + }) + + it('holds a session tabs unsubscribe until the host registers the stream', () => { + const { registry, sent } = createRegistry() + const events: unknown[] = [] + const dispose = registry.subscribe('session.tabs.subscribe', { worktree: 'wt-1' }, (event) => + events.push(event) + ) + const subscribe = sent[0]! + + dispose() + // Older hosts register only as they emit the first snapshot, so an earlier unsubscribe finds nothing. + expect(sent).toHaveLength(1) + + registry.handleResponse(streamingResponse(subscribe.id, { type: 'snapshot', tabs: [] })) + + expect(sent[1]).toMatchObject({ + method: 'session.tabs.unsubscribe', + params: { worktree: 'wt-1', subscriptionId: subscribe.id } + }) + // A host that registered on arrival ends the stream; that end must not unsubscribe again. + registry.handleResponse(streamingResponse(subscribe.id, { type: 'end' })) + expect(sent).toHaveLength(2) + expect(events).toEqual([]) + expect(registry.size()).toBe(0) + }) + + it('holds a session tabs unsubscribe again after a reconnect replays the stream', () => { + const { registry, sent } = createRegistry() + const dispose = registry.subscribe('session.tabs.subscribe', { worktree: 'wt-1' }, () => {}) + const subscribe = sent[0]! + registry.handleResponse(streamingResponse(subscribe.id, { type: 'snapshot', tabs: [] })) + + registry.markForReplay() + registry.replayAfterAuthentication() + dispose() + + expect(sent.map((request) => request.method)).toEqual([ + 'session.tabs.subscribe', + 'session.tabs.subscribe' + ]) + registry.handleResponse(streamingResponse(subscribe.id, { type: 'snapshot', tabs: [] })) + expect(sent[2]).toMatchObject({ + method: 'session.tabs.unsubscribe', + params: { worktree: 'wt-1', subscriptionId: subscribe.id } + }) + }) + + it('drops a held session tabs unsubscribe when the subscribe fails or reconnects first', () => { + const { registry, sent } = createRegistry() + const events: unknown[] = [] + const disposeFailed = registry.subscribe( + 'session.tabs.subscribe', + { worktree: 'wt-1' }, + (event) => events.push(event) + ) + const disposeReplayed = registry.subscribe( + 'session.tabs.subscribe', + { worktree: 'wt-2' }, + () => {} + ) + const failed = sent[0]! + disposeFailed() + disposeReplayed() + + registry.handleResponse({ + id: failed.id, + ok: false, + error: { code: 'worktree_not_found', message: 'Worktree not found' } + }) + registry.markForReplay() + registry.replayAfterAuthentication() + + expect(sent).toHaveLength(2) + expect(events).toEqual([]) + expect(registry.size()).toBe(0) + }) + it('ends one transcript stream on dispose and leaves a sibling on the same socket (U-03)', () => { const { registry, sent } = createRegistry() const disposeFirst = registry.subscribe('agentSession.subscribe', { sessionId: 's1' }, () => {}) diff --git a/mobile/src/transport/rpc-client-stream-registry.ts b/mobile/src/transport/rpc-client-stream-registry.ts index ebfdb741997..4062e80b49b 100644 --- a/mobile/src/transport/rpc-client-stream-registry.ts +++ b/mobile/src/transport/rpc-client-stream-registry.ts @@ -14,6 +14,7 @@ import { isTerminalSubscribedResult } from './rpc-subscription-result-shapes' import { RpcClientTerminalStreamRouter } from './rpc-client-terminal-stream-router' +import * as sessionTabsStream from './rpc-client-session-tabs-stream' import type { ConnectionState, RpcResponse, RpcSuccess } from './types' export type RpcStreamingListener = (result: unknown) => void @@ -30,6 +31,7 @@ type StreamRequest = { subscriptionId?: string cancelled?: boolean sent?: boolean + receivedSnapshot?: boolean } type StreamRegistryOptions = { @@ -108,6 +110,7 @@ export class RpcClientStreamRegistry { this.pendingBrowserRequestId = null for (const [id, stream] of this.streams) { stream.sent = false + stream.receivedSnapshot = false // The id named a registration on the closed socket; the replay's ready brings the new one. stream.subscriptionId = undefined this.resetTerminalRouting(id) @@ -169,6 +172,10 @@ export class RpcClientStreamRegistry { return } const result = response.result + if (sessionTabsStream.recordSnapshot(stream, result) && stream.cancelled) { + this.dispose(response.id) + return + } if (isStreamingSubscriptionReadyResult(result)) { stream.subscriptionId = result.subscriptionId if (stream.cancelled) { @@ -215,6 +222,8 @@ export class RpcClientStreamRegistry { // Why: `requestId` names this exact request; hosts that predate it strip it and use the slot. this.sendRpc('terminal.unsubscribe', { ...params, requestId: id }) } + } else if (stream && sessionTabsStream.holdUnsubscribe(stream)) { + return } else { const unsubscribe = buildStreamUnsubscribe(stream?.method, stream?.params, id) if (unsubscribe) { diff --git a/mobile/src/transport/rpc-client.test.ts b/mobile/src/transport/rpc-client.test.ts index e88e6c220e6..ff290f72b84 100644 --- a/mobile/src/transport/rpc-client.test.ts +++ b/mobile/src/transport/rpc-client.test.ts @@ -196,12 +196,21 @@ describe('mobile rpc-client connection timeout', () => { { worktree: 'id:wt-1' }, () => {} ) + const request = sentRequest(socket, 'session.tabs.subscribe') + socket.receive( + `encrypted:${JSON.stringify({ + id: request.id, + ok: true, + streaming: true, + result: { type: 'snapshot', worktree: 'id:wt-1', tabs: [] }, + _meta: { runtimeId: 'r1' } + })}` + ) unsubscribe() - expect( - socket.sent.some((payload) => payload.includes('"method":"session.tabs.unsubscribe"')) - ).toBe(true) - expect(socket.sent.some((payload) => payload.includes('"worktree":"id:wt-1"'))).toBe(true) + expect(sentRequests(socket, 'session.tabs.unsubscribe')).toEqual([ + expect.objectContaining({ params: { worktree: 'id:wt-1', subscriptionId: request.id } }) + ]) client.close() }) diff --git a/mobile/src/transport/rpc-subscription-result-shapes.ts b/mobile/src/transport/rpc-subscription-result-shapes.ts index cf09acdb6ca..4c624658126 100644 --- a/mobile/src/transport/rpc-subscription-result-shapes.ts +++ b/mobile/src/transport/rpc-subscription-result-shapes.ts @@ -19,3 +19,7 @@ export function isStreamingSubscriptionReadyResult( typeof (value as { subscriptionId?: unknown }).subscriptionId === 'string' ) } + +export function isSnapshotResult(value: unknown): value is { type: 'snapshot' } { + return typeof value === 'object' && value !== null && 'type' in value && value.type === 'snapshot' +} diff --git a/src/cli/handlers/orchestration/worker-output.ts b/src/cli/handlers/orchestration/worker-output.ts index 9d6d1949911..d9b5b9a46fa 100644 --- a/src/cli/handlers/orchestration/worker-output.ts +++ b/src/cli/handlers/orchestration/worker-output.ts @@ -1,6 +1,6 @@ import type { RuntimeTerminalRead } from '../../../shared/runtime-types' import type { OrchestrationWorkerReadResult } from '../../../shared/orchestration-worker-output' -import { formatWorkerTranscriptMessage } from '../../../shared/worker-transcript-text' +import { formatWorkerTranscriptMessages } from '../../../shared/worker-transcript-text' export type LegacyWorkerReadResult = { dispatchId: string @@ -57,7 +57,7 @@ export function formatWorkerRead( const output = value.source === 'terminal' ? value.terminal.tail.join('\n') - : value.transcript.messages.map(formatWorkerTranscriptMessage).join('\n\n') + : formatWorkerTranscriptMessages(value.transcript.messages).join('\n\n') if (output) { return `${details}\n\n${output}` } diff --git a/src/cli/handlers/worktree.ts b/src/cli/handlers/worktree.ts index 62ddd27155a..3541beef20b 100644 --- a/src/cli/handlers/worktree.ts +++ b/src/cli/handlers/worktree.ts @@ -245,7 +245,8 @@ export const WORKTREE_HANDLERS: Record = { ...(startupAgent ? { startupAgent, - startupPrompt: getPresentStringFlag(flags, 'prompt', { allowEmpty: true }) ?? '' + startupPrompt: getPresentStringFlag(flags, 'prompt', { allowEmpty: true }) ?? '', + launchSource: 'cli' } : {}) }) diff --git a/src/cli/index-worktree-create-agent.test.ts b/src/cli/index-worktree-create-agent.test.ts index f1d79ef21df..6ae1266b91c 100644 --- a/src/cli/index-worktree-create-agent.test.ts +++ b/src/cli/index-worktree-create-agent.test.ts @@ -138,7 +138,8 @@ describe('orca cli worktree awareness', () => { callerTerminalHandle: undefined, cliProvenanceRequest: {}, startupAgent: 'codex', - startupPrompt: 'hi' + startupPrompt: 'hi', + launchSource: 'cli' }) }) @@ -187,7 +188,8 @@ describe('orca cli worktree awareness', () => { callerTerminalHandle: undefined, cliProvenanceRequest: {}, startupAgent: 'codex', - startupPrompt: 'hi' + startupPrompt: 'hi', + launchSource: 'cli' }) }) diff --git a/src/cli/search-command-arguments.test.ts b/src/cli/search-command-arguments.test.ts index be9195a1fab..27b06286f52 100644 --- a/src/cli/search-command-arguments.test.ts +++ b/src/cli/search-command-arguments.test.ts @@ -126,7 +126,7 @@ describe('orca search argument parsing', () => { it('rejects an unknown --agent and names the known ones', () => { expect(() => parseSearch(['search', 'q', '--agent', 'claude', '--agent', 'bogus'])).toThrow( - /Unknown --agent "bogus"\. Known agents: claude, codex, / + /Unknown --agent "bogus"\. Known agents: claude, codebuddy, codex, / ) }) diff --git a/src/main/agent-hooks/managed-agent-hook-registry.ts b/src/main/agent-hooks/managed-agent-hook-registry.ts index 31513658035..11b642ee090 100644 --- a/src/main/agent-hooks/managed-agent-hook-registry.ts +++ b/src/main/agent-hooks/managed-agent-hook-registry.ts @@ -1,3 +1,4 @@ +import { codebuddyHookService } from '../codebuddy/hook-service' import { qoderHookService } from '../qoder/hook-service' import type { AgentHookInstallStatus } from '../../shared/agent-hook-types' import type { HookInstallAgent } from '../../shared/telemetry-events' @@ -46,6 +47,7 @@ export const MANAGED_AGENT_HOOK_INSTALLERS: readonly ManagedAgentHookInstaller[] ['codex', () => codexHookService.install()], ['gemini', () => geminiHookService.install()], ['qoder', () => qoderHookService.install()], + ['codebuddy', () => codebuddyHookService.install()], ['antigravity', () => antigravityHookService.install()], ['amp', () => ampHookService.install()], ['cursor', () => cursorHookService.install()], @@ -73,6 +75,7 @@ export const MANAGED_AGENT_HOOK_SCRIPT_REFRESHERS: readonly ManagedAgentHookScri ['codex', () => codexHookService.refreshManagedScripts()], ['gemini', () => geminiHookService.refreshManagedScripts()], ['qoder', () => qoderHookService.refreshManagedScripts()], + ['codebuddy', () => codebuddyHookService.refreshManagedScripts()], ['antigravity', () => antigravityHookService.refreshManagedScripts()], ['cursor', () => cursorHookService.refreshManagedScripts()], ['droid', () => droidHookService.refreshManagedScripts()], @@ -92,6 +95,7 @@ export const MANAGED_AGENT_HOOK_REMOVERS: readonly ManagedAgentHookRemover[] = [ ['codex', () => codexHookService.remove()], ['gemini', () => geminiHookService.remove()], ['qoder', () => qoderHookService.remove()], + ['codebuddy', () => codebuddyHookService.remove()], ['antigravity', () => antigravityHookService.remove()], ['amp', () => ampHookService.remove()], ['cursor', () => cursorHookService.remove()], @@ -117,6 +121,7 @@ export const MANAGED_AGENT_HOOK_STATUS_READERS: readonly ManagedAgentHookStatusR ['codex', () => codexHookService.getStatus()], ['gemini', () => geminiHookService.getStatus()], ['qoder', () => qoderHookService.getStatus()], + ['codebuddy', () => codebuddyHookService.getStatus()], ['antigravity', () => antigravityHookService.getStatus()], ['amp', () => ampHookService.getStatus()], ['cursor', () => cursorHookService.getStatus()], diff --git a/src/main/agent-hooks/managed-hook-command-contract.test.ts b/src/main/agent-hooks/managed-hook-command-contract.test.ts index b3166a6c90e..ebb1412ec58 100644 --- a/src/main/agent-hooks/managed-hook-command-contract.test.ts +++ b/src/main/agent-hooks/managed-hook-command-contract.test.ts @@ -96,6 +96,20 @@ const buildersByAgent = new Map([ remote: (path) => [getClaudeRemoteCommand(path)] } ], + [ + 'codebuddy', + { + local: (path) => [ + getManagedLifecycleHook(path, { + configDirName: '.codebuddy', + scriptBaseName: 'codebuddy-hook', + usesWindowsCompatLauncher: true, + windowsHookShell: 'powershell' + }).command + ], + remote: (path) => [getClaudeRemoteCommand(path)] + } + ], [ 'codex', { @@ -224,7 +238,8 @@ describe('managed hook command contract', () => { const scannedCommand = platform === 'win32' && ((agent === 'codex' && command.startsWith('if (Test-Path')) || - (agent === 'qoder' && command.startsWith('$scriptPath = Join-Path'))) + ((agent === 'qoder' || agent === 'codebuddy') && + command.startsWith('$scriptPath = Join-Path'))) ? command .replaceAll('$LASTEXITCODE', '') .replaceAll('$env:', '') diff --git a/src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts b/src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts index f9ff9c232e0..dea6c4a4663 100644 --- a/src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts +++ b/src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts @@ -1,3 +1,4 @@ +import { codebuddyHookService } from '../codebuddy/hook-service' import { describe, expect, it, vi } from 'vitest' vi.mock('electron', () => ({ @@ -53,6 +54,7 @@ describe('remote hook service registry coverage', () => { ['kimi', kimiHookService], ['muse', museHookService], ['qoder', qoderHookService], + ['codebuddy', codebuddyHookService], ['zcode', zcodeHookService], ['dsh', dshHookService] ]) diff --git a/src/main/agent-hooks/remote-managed-hook-installers.ts b/src/main/agent-hooks/remote-managed-hook-installers.ts index 272e330be11..b012af91ff3 100644 --- a/src/main/agent-hooks/remote-managed-hook-installers.ts +++ b/src/main/agent-hooks/remote-managed-hook-installers.ts @@ -1,3 +1,4 @@ +import { codebuddyHookService } from '../codebuddy/hook-service' import { qoderHookService } from '../qoder/hook-service' import type { SFTPWrapper } from 'ssh2' import type { AgentHookInstallStatus, AgentHookTarget } from '../../shared/agent-hook-types' @@ -64,6 +65,7 @@ const REMOTE_MANAGED_HOOK_INSTALLERS: readonly RemoteManagedHookInstaller[] = [ ], ['gemini', (sftp, remoteHome) => geminiHookService.installRemote(sftp, remoteHome)], ['qoder', (sftp, remoteHome) => qoderHookService.installRemote(sftp, remoteHome)], + ['codebuddy', (sftp, remoteHome) => codebuddyHookService.installRemote(sftp, remoteHome)], ['antigravity', (sftp, remoteHome) => antigravityHookService.installRemote(sftp, remoteHome)], ['amp', (sftp, remoteHome) => ampHookService.installRemote(sftp, remoteHome)], ['cursor', (sftp, remoteHome) => cursorHookService.installRemote(sftp, remoteHome)], diff --git a/src/main/agent-hooks/server-retired-pane-new-turn.test.ts b/src/main/agent-hooks/server-retired-pane-new-turn.test.ts index a405199a6e7..23dbb56c695 100644 --- a/src/main/agent-hooks/server-retired-pane-new-turn.test.ts +++ b/src/main/agent-hooks/server-retired-pane-new-turn.test.ts @@ -25,6 +25,7 @@ const NEW_TURN_EVENT: Record = { qoder: 'SessionStart', claude: 'SessionStart', kimi: 'UserPromptSubmit', + codebuddy: 'UserPromptSubmit', codex: 'SessionStart', gemini: 'BeforeAgent', antigravity: 'PreInvocation', diff --git a/src/main/agent-hooks/server/server-row-ownership.test.ts b/src/main/agent-hooks/server/server-row-ownership.test.ts new file mode 100644 index 00000000000..e466ef879d2 --- /dev/null +++ b/src/main/agent-hooks/server/server-row-ownership.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from 'vitest' +import { AgentHookServer } from '../server' +import type { AgentHookStatusRowMutation, EnrichedAgentHookEventPayload } from './server-types' + +const PANE_KEY = 'tab-row:55555555-5555-4555-8555-555555555555' + +class RowMutationProbe extends AgentHookServer { + readonly mutations: AgentHookStatusRowMutation[] = [] + + constructor() { + super() + this.subscribeStatusRowMutations((mutation) => this.mutations.push(mutation)) + } + + commit( + before: EnrichedAgentHookEventPayload | null | undefined, + after: EnrichedAgentHookEventPayload | null | undefined + ): boolean { + return this.commitStatusRowMutation(before, after) + } +} + +function row( + overrides: Partial = {} +): EnrichedAgentHookEventPayload { + return { + paneKey: PANE_KEY, + tabId: 'tab-row', + worktreeId: 'worktree', + connectionId: null, + receivedAt: 1_000, + stateStartedAt: 900, + terminalHandle: 'term_row', + ...overrides, + payload: { + state: 'working', + prompt: 'ship it', + agentType: 'opencode', + lastAssistantMessage: 'x'.repeat(8_000), + subagents: [{ id: 'child-1', state: 'working', agentType: 'claude', startedAt: 800 }], + ...overrides.payload + } + } +} + +describe('status-row change detection', () => { + it('treats the same row object on both sides as unchanged', () => { + const probe = new RowMutationProbe() + const same = row() + expect(probe.commit(same, same)).toBe(false) + expect(probe.commit(null, undefined)).toBe(false) + expect(probe.mutations).toEqual([]) + }) + + it('ignores a rebuilt row whose published content is identical', () => { + const probe = new RowMutationProbe() + const before = row() + const rebuilt = row({ + receivedAt: 2_000, + evidenceObservedAt: 1_500, + launchToken: 'launch-2', + promptInteractionKey: 'turn-2', + observation: { + origin: 'hook', + authorityId: 'authority', + incarnation: 1, + revision: 7, + observedAt: 2_000 + }, + payload: { ...before.payload, subagents: before.payload.subagents?.map((s) => ({ ...s })) } + }) + expect(probe.commit(before, rebuilt)).toBe(false) + expect(probe.mutations).toEqual([]) + }) + + it('ignores key order, which a producer is free to change', () => { + const probe = new RowMutationProbe() + const before = row() + const reordered: EnrichedAgentHookEventPayload = { + payload: { + subagents: before.payload.subagents?.map((s) => ({ ...s })), + lastAssistantMessage: before.payload.lastAssistantMessage, + agentType: before.payload.agentType, + prompt: before.payload.prompt, + state: before.payload.state + }, + terminalHandle: before.terminalHandle, + stateStartedAt: before.stateStartedAt, + receivedAt: before.receivedAt, + connectionId: before.connectionId, + worktreeId: before.worktreeId, + tabId: before.tabId, + paneKey: before.paneKey + } + expect(probe.commit(before, reordered)).toBe(false) + }) + + // Why: paired clients read the whole payload off session.tabs, so no field may be skipped. + it.each<[string, Partial]>([ + ['state', { payload: { state: 'done', prompt: 'ship it' } }], + [ + 'lastAssistantMessage', + { payload: { state: 'working', prompt: 'ship it', lastAssistantMessage: 'y' } } + ], + [ + 'subagents', + { + payload: { + state: 'working', + prompt: 'ship it', + subagents: [{ id: 'child-1', state: 'idle', agentType: 'claude', startedAt: 800 }] + } + } + ], + [ + 'a newly present payload field', + { payload: { state: 'working', prompt: 'ship it', model: 'm' } } + ], + ['stateStartedAt', { stateStartedAt: 950 }], + ['connectionId', { connectionId: 'ssh-a' }], + ['terminalHandle', { terminalHandle: 'term_other' }], + ['providerSession', { providerSession: { key: 'session_id', id: 's-1' } }], + ['structuredHost', { structuredHost: 'owned' }] + ])('publishes a change to %s', (_field, overrides) => { + const probe = new RowMutationProbe() + const before = row() + expect(probe.commit(before, row(overrides))).toBe(true) + expect(probe.mutations).toEqual([ + { + before: { paneKey: PANE_KEY, worktreeId: 'worktree', terminalHandle: 'term_row' }, + after: expect.objectContaining({ paneKey: PANE_KEY }) + } + ]) + }) + + it('publishes a row appearing or disappearing', () => { + const probe = new RowMutationProbe() + expect(probe.commit(null, row())).toBe(true) + expect(probe.commit(row(), undefined)).toBe(true) + expect(probe.mutations).toHaveLength(2) + }) +}) diff --git a/src/main/agent-hooks/server/server-row-ownership.ts b/src/main/agent-hooks/server/server-row-ownership.ts index 2895eb463e4..cde178e335d 100644 --- a/src/main/agent-hooks/server/server-row-ownership.ts +++ b/src/main/agent-hooks/server/server-row-ownership.ts @@ -4,6 +4,7 @@ import { } from '../../../shared/wsl-hook-relay-contract' import { splitWorktreeIdForFilesystem, worktreeIdsEqual } from '../../../shared/worktree/id' import { parseWslUncPath } from '../../../shared/wsl-paths' +import { structuralValuesEqualIgnoringUndefined } from '../../../shared/structural-value-equality' import type { AgentHookEventPayload } from '../../../shared/agent-hook-listener/listener-event' import type { AgentHookStatusRowIdentity, @@ -27,10 +28,7 @@ function toMutationIdentity( } } -function semanticRowJson(row: EnrichedAgentHookEventPayload | null | undefined): string | null { - if (!row) { - return null - } +function semanticRow(row: EnrichedAgentHookEventPayload): Record { const { receivedAt: _receivedAt, evidenceObservedAt: _evidenceObservedAt, @@ -39,7 +37,22 @@ function semanticRowJson(row: EnrichedAgentHookEventPayload | null | undefined): promptInteractionKey: _promptInteractionKey, ...semantic } = toAgentStatusIpcPayload(row) - return JSON.stringify(semantic) + return semantic +} + +// Why: runs on every status write; a structural walk exits on the first difference and skips +// shared leaves (e.g. an unchanged 8 KB lastAssistantMessage) instead of serializing both rows. +function semanticRowsEqual( + before: EnrichedAgentHookEventPayload | null | undefined, + after: EnrichedAgentHookEventPayload | null | undefined +): boolean { + if (before === after || (!before && !after)) { + return true + } + if (!before || !after) { + return false + } + return structuralValuesEqualIgnoringUndefined(semanticRow(before), semanticRow(after)) } function wslDistroForWorktree(worktreeId: string | undefined): string | null { @@ -113,7 +126,7 @@ export abstract class AgentHookServerRowOwnership extends AgentHookServerListene if (after?.terminalHandle) { this.paneKeyByTerminalHandle.set(after.terminalHandle, after.paneKey) } - if (!emit || semanticRowJson(before) === semanticRowJson(after)) { + if (!emit || semanticRowsEqual(before, after)) { return false } const mutation: AgentHookStatusRowMutation = { diff --git a/src/main/agent-launch/__fixtures__/host-agent-startup-call-sites.txt b/src/main/agent-launch/__fixtures__/host-agent-startup-call-sites.txt new file mode 100644 index 00000000000..de078fd731e --- /dev/null +++ b/src/main/agent-launch/__fixtures__/host-agent-startup-call-sites.txt @@ -0,0 +1,10 @@ +# Host-side calls to the agent startup-plan builders, per file: . +# attributes a fresh agent the host builds; carries agentStartedTelemetry +# resume continues an existing agent session; not a new start +# mobile-followup the phone's session-tab launch; attribution is a separate follow-up +# The resolve-worktree-removal-target call also serves a bare typed command, which stays unattributed. +src/main/runtime/orca-runtime-create-agent-session.ts 2 attributes +src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts 1 attributes +src/main/runtime/runtime-worktree-agent-startup.ts 3 attributes +src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts 1 resume +src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts 1 mobile-followup diff --git a/src/main/agent-launch/agent-started-telemetry.ts b/src/main/agent-launch/agent-started-telemetry.ts new file mode 100644 index 00000000000..2d1ff76179d --- /dev/null +++ b/src/main/agent-launch/agent-started-telemetry.ts @@ -0,0 +1,30 @@ +/** + * The `agent_started` triple for an agent launch the host builds, of which only `launch_source` + * comes from the caller. + * + * `agent_kind` and `request_kind` are derived rather than accepted — the host already knows both, + * and a value it derives is a value a caller cannot misreport. `request_kind` is always `new` + * because a host-built launch is always a fresh agent. + * + * Every launch is attributed: a caller that names no surface, or one this build has never heard of + * (a newer client), is recorded as `unknown` rather than dropped. The wire keeps the arm set open so + * an older host cannot refuse a newer client's launch over a label, and resolving it here, leniently, + * keeps that label from gating the launch at the point it is actually used. + */ + +import { tuiAgentToAgentKind } from '../../shared/agent-kind' +import { launchSourceSchema } from '../../shared/telemetry-property-schemas' +import type { TuiAgent } from '../../shared/tui-agent' +import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' + +export function agentStartedTelemetry( + agent: TuiAgent, + launchSource: string | undefined +): NonNullable { + const parsed = launchSourceSchema.safeParse(launchSource) + return { + agent_kind: tuiAgentToAgentKind(agent), + launch_source: parsed.success ? parsed.data : 'unknown', + request_kind: 'new' + } +} diff --git a/src/main/agent-launch/host-agent-startup-attribution.test.ts b/src/main/agent-launch/host-agent-startup-attribution.test.ts new file mode 100644 index 00000000000..612caab8001 --- /dev/null +++ b/src/main/agent-launch/host-agent-startup-attribution.test.ts @@ -0,0 +1,99 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { join, relative, resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +/** + * Every host-side call that builds an agent's startup command has made an attribution decision. + * + * `agent_started` is recorded per call site, and twice a new host builder shipped without one, so + * that agent's launches were silently uncounted. A new call, or another call in a listed file, + * fails here until its author decides and records whether that launch is a fresh start. + */ +const DECISIONS = ['attributes', 'resume', 'mobile-followup'] as const + +const LISTED: ReadonlyMap = new Map( + readFileSync(join(__dirname, '__fixtures__', 'host-agent-startup-call-sites.txt'), 'utf8') + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith('#')) + .map((line) => { + const [path, calls, decision] = line.split(/\s+/) + return [path, { calls: Number(calls), decision }] as const + }) +) + +const BUILDER_CALL = + /\b(?:buildAgentStartupPlan|buildAgentDraftLaunchPlan|buildAgentResumeStartupPlan)\s*\(/g + +const DECIDE = + 'Decide attribution: a fresh agent the host builds must carry ' + + '`agentStartedTelemetry(agent, launchSource)` (src/main/agent-launch/agent-started-telemetry.ts) ' + + 'on its startup; a resume or a bare typed command must not. Then record the file, its call ' + + 'count and the decision in __fixtures__/host-agent-startup-call-sites.txt.' + +function isTestFile(path: string): boolean { + return /\.(?:test|spec)\.tsx?$/.test(path) || path.includes('/__tests__/') +} + +function collectSourceFiles(root: string): string[] { + return readdirSync(root).flatMap((entry) => { + const full = join(root, entry) + if (statSync(full).isDirectory()) { + return entry === '__fixtures__' ? [] : collectSourceFiles(full) + } + return /\.tsx?$/.test(entry) ? [full] : [] + }) +} + +/** Drop comment-only lines so prose naming a builder is not a call. */ +function codeText(contents: string): string { + return contents + .split('\n') + .filter((line) => !/^\s*(?:\/\/|\/\*|\*)/.test(line)) + .join('\n') +} + +describe('host agent startup attribution', () => { + const repoRoot = resolve(__dirname, '..', '..', '..') + const files = collectSourceFiles(join(repoRoot, 'src', 'main')) + const found = new Map() + for (const file of files) { + const path = relative(repoRoot, file).split('\\').join('/') + const calls = isTestFile(path) + ? 0 + : (codeText(readFileSync(file, 'utf8')).match(BUILDER_CALL)?.length ?? 0) + if (calls > 0) { + found.set(path, calls) + } + } + + it('scans a plausible number of files', () => { + // A broken root would make the guard silently vacuous. + expect(files.length).toBeGreaterThan(500) + }) + + it('has an attribution decision for every builder call', () => { + const undecided = [...found] + .filter(([path, calls]) => calls > (LISTED.get(path)?.calls ?? 0)) + .map(([path, calls]) => `${path}: ${calls} calls, ${LISTED.get(path)?.calls ?? 0} decided`) + expect(undecided, `New host-built agent startup. ${DECIDE}`).toEqual([]) + }) + + it('has no stale entry', () => { + // A count left above reality lets the next call in that file land without a decision. + const stale = [...LISTED] + .filter(([path, entry]) => entry.calls > (found.get(path) ?? 0)) + .map(([path, entry]) => `${path}: ${found.get(path) ?? 0} calls, ${entry.calls} listed`) + expect(stale, 'Lower or delete the entry to match the calls that remain.').toEqual([]) + }) + + it('uses the known decision vocabulary', () => { + const unknown = [...LISTED].filter( + ([, entry]) => !DECISIONS.some((decision) => decision === entry.decision) + ) + expect( + unknown.map(([path]) => path), + `Use one of: ${DECISIONS.join(', ')}.` + ).toEqual([]) + }) +}) diff --git a/src/main/ai-vault/remote-session-scanner-sources.ts b/src/main/ai-vault/remote-session-scanner-sources.ts index ed34e8b52a5..dfd012415d5 100644 --- a/src/main/ai-vault/remote-session-scanner-sources.ts +++ b/src/main/ai-vault/remote-session-scanner-sources.ts @@ -8,6 +8,7 @@ import { isAntigravityTranscriptPath } from './session-scanner-antigravity-paths import { parseCodexSessionContent } from './session-scanner-codex-parser' import { parseDroidSessionContent } from './session-scanner-droid-parser' import { parseClaudeSessionContent } from './session-scanner-primary-parsers' +import { parseCodebuddySessionContent } from './session-scanner-codebuddy-parser' import { parseGeminiSessionContent } from './session-scanner-gemini-parsers' import { parseCopilotSessionContent } from './session-scanner-copilot-parser' import { parseCursorSessionContent } from './session-scanner-cursor-parser' @@ -65,6 +66,17 @@ export function remoteSessionSources( // top-level sessions carrying the parent's sessionId. partitionSubagentTranscripts: partitionSubagentTranscriptPaths }, + { + // The execution host owns CodeBuddy's transcripts, including subagent pruning. + ...jsonlSource( + 'codebuddy', + remoteHome, + hostPlatform, + ['.codebuddy', 'projects'], + parseCodebuddySessionContent + ), + partitionSubagentTranscripts: partitionSubagentTranscriptPaths + }, remoteAntigravitySource(remoteHome, hostPlatform), source( 'gemini', diff --git a/src/main/ai-vault/session-cwd-bucket-layouts.test.ts b/src/main/ai-vault/session-cwd-bucket-layouts.test.ts index 38c2f0b68e2..7cbbbfbcc6d 100644 --- a/src/main/ai-vault/session-cwd-bucket-layouts.test.ts +++ b/src/main/ai-vault/session-cwd-bucket-layouts.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { PI_CWD_BUCKET_LAYOUT } from './session-cwd-bucket-layouts' +import { PI_CWD_BUCKET_LAYOUT, CODEBUDDY_CWD_BUCKET_LAYOUT } from './session-cwd-bucket-layouts' function piDirInScope(dirName: string, scopePath: string): boolean { return PI_CWD_BUCKET_LAYOUT.isDirInScope( @@ -31,3 +31,19 @@ describe('PI_CWD_BUCKET_LAYOUT', () => { expect(piDirInScope('--C--Users-ada-repo--', 'C:\\Users\\ada\\repo')).toBe(true) }) }) + +describe('CODEBUDDY_CWD_BUCKET_LAYOUT', () => { + it.each([ + ['C:\\Users\\ada\\repo', 'C-Users-ada-repo-src'], + ['/home/ada/repo', 'home-ada-repo-src'], + ['/', 'home-ada-repo'], + [`/${'界'.repeat(100)}`, `${'界'.repeat(60)}-hash`] + ])('keeps candidate buckets for %s', (scope, bucket) => { + expect( + CODEBUDDY_CWD_BUCKET_LAYOUT.isDirInScope( + bucket, + new Set(CODEBUDDY_CWD_BUCKET_LAYOUT.encodeScopePrefixes(scope)) + ) + ).toBe(true) + }) +}) diff --git a/src/main/ai-vault/session-cwd-bucket-layouts.ts b/src/main/ai-vault/session-cwd-bucket-layouts.ts index d8b4a565949..3f0195cdcd4 100644 --- a/src/main/ai-vault/session-cwd-bucket-layouts.ts +++ b/src/main/ai-vault/session-cwd-bucket-layouts.ts @@ -3,7 +3,7 @@ import { encodeClaudeProjectPaths, isClaudeProjectDirInScope } from './claude-pr /** An agent that keeps one directory per cwd, named by a lossy encoding of that cwd. */ export type CwdBucketLayout = { - agent: Extract + agent: Extract encodeScopePrefixes: (scopePath: string) => string[] isDirInScope: (dirName: string, prefixes: ReadonlySet) => boolean } @@ -41,3 +41,28 @@ export const PI_CWD_BUCKET_LAYOUT: CwdBucketLayout = { return false } } + +// CodeBuddy 2.159.0 collapses separators and retains 180 UTF-8 bytes before long-path hashes. +function codebuddyScopePrefix(path: string): string { + const encoded = path + .replace(/[/\\:]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + let prefix = '' + for (const character of encoded) { + if (Buffer.byteLength(prefix + character, 'utf8') > 180) { + break + } + prefix += character + } + return prefix +} + +export const CODEBUDDY_CWD_BUCKET_LAYOUT: CwdBucketLayout = { + agent: 'codebuddy', + encodeScopePrefixes: (path) => [ + ...new Set([path, path.normalize('NFC')].map(codebuddyScopePrefix)) + ], + // This is only a prefilter: each transcript's cwd decides membership, including lossy collisions. + isDirInScope: (name, prefixes) => [...prefixes].some((prefix) => name.startsWith(prefix)) +} diff --git a/src/main/ai-vault/session-scanner-agent-parser.ts b/src/main/ai-vault/session-scanner-agent-parser.ts index 80a56bf2519..95910d208b7 100644 --- a/src/main/ai-vault/session-scanner-agent-parser.ts +++ b/src/main/ai-vault/session-scanner-agent-parser.ts @@ -1,3 +1,4 @@ +import { parseCodebuddySessionFile } from './session-scanner-codebuddy-parser' import type { AiVaultSession } from '../../shared/ai-vault-types' import { throwIfSignalAborted } from '../../shared/abort-signal-reason' import { parseDevinSessionFile } from './session-scanner-devin-parser' @@ -87,6 +88,8 @@ export async function parseAgentSessionFile( signal?: AbortSignal ): Promise { switch (candidate.agent) { + case 'codebuddy': + return parseCodebuddySessionFile(candidate.file, platform, messages) case 'claude': return parseClaudeSessionFile(candidate.file, platform, messages) case 'codex': diff --git a/src/main/ai-vault/session-scanner-agent-sources.ts b/src/main/ai-vault/session-scanner-agent-sources.ts index ee10422031b..66d069b68cc 100644 --- a/src/main/ai-vault/session-scanner-agent-sources.ts +++ b/src/main/ai-vault/session-scanner-agent-sources.ts @@ -34,6 +34,7 @@ const COPILOT_SESSIONS_DIR = join( 'session-state' ) const CURSOR_PROJECTS_DIR = join(homedir(), '.cursor', 'projects') +const CODEBUDDY_PROJECTS_DIR = join(homedir(), '.codebuddy', 'projects') const HERMES_SESSIONS_DIR = join(homedir(), '.hermes', 'sessions') const ROVO_SESSIONS_DIR = join(homedir(), '.rovodev', 'sessions') const OPENCLAW_STATE_DIR = resolveAbsoluteDirOverride( @@ -114,6 +115,15 @@ export const AI_VAULT_AGENT_SOURCES: AiVaultAgentSourceTable = { // their parent instead. directoryPredicate: (name) => name !== SUBAGENT_DIR_NAME }, + codebuddy: { + rootDirs: (options, wslHomeDirs) => [ + options.codebuddyProjectsDir ?? CODEBUDDY_PROJECTS_DIR, + ...wslHomeDirs.map((homeDir) => join(homeDir, '.codebuddy', 'projects')) + ], + extensions: ['.jsonl'], + // Nested subagent transcripts are not independent top-level conversations. + directoryPredicate: (name) => name !== SUBAGENT_DIR_NAME + }, codex: { rootDirs: (options, wslHomeDirs) => uniqueCodexSessionsDirs([ diff --git a/src/main/ai-vault/session-scanner-codebuddy-parser.ts b/src/main/ai-vault/session-scanner-codebuddy-parser.ts new file mode 100644 index 00000000000..94fc52d1eab --- /dev/null +++ b/src/main/ai-vault/session-scanner-codebuddy-parser.ts @@ -0,0 +1,230 @@ +import { createInterface } from 'node:readline' +import { + remoteSessionContentLines, + type RemoteSessionContent +} from './remote-session-content-lines' +import { openTranscriptReadStream } from '../native-chat/wsl-transcript-fs-access' +import type { AiVaultSession } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' +import { isKnownHarnessInjectedUserTurnText } from '../../shared/harness-injected-user-turns' +import type { FileWithMtime, ResumableSessionParseState } from './session-scanner-types' +import type { TranscriptMessageSink } from './session-transcript-consumers' +import { + addPreviewMessage, + accumulatorSessionIdentity, + createAccumulator, + finalizeSession, + sessionIdFromFileName, + updateLatestLocation, + updateTimeline +} from './session-scanner-accumulator' +import { + asRecord, + extractString, + normalizeTitleText, + parseJsonObject +} from './session-scanner-values' + +type ParserSessionOptions = { + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null +} + +// CodeBuddy stores top-level message roles and input_text/output_text blocks, not Claude records. +export type CodebuddySessionParseState = { + accumulator: ReturnType + aiTitle: string | null + firstUserTitle: string | null +} + +export function createCodebuddySessionParseState( + file: FileWithMtime, + messages?: TranscriptMessageSink +): CodebuddySessionParseState { + return { + accumulator: createAccumulator({ + agent: 'codebuddy', + file, + sessionId: sessionIdFromFileName(file.path), + messages + }), + aiTitle: null, + firstUserTitle: null + } +} + +export function cloneCodebuddySessionParseState( + state: CodebuddySessionParseState +): CodebuddySessionParseState { + return { + accumulator: { + ...state.accumulator, + previewMessages: [...state.accumulator.previewMessages] + }, + aiTitle: state.aiTitle, + firstUserTitle: state.firstUserTitle + } +} + +// CodeBuddy conversation content is an array of typed text blocks; join the +// text ones. Non-text blocks (tool calls, diffs) carry no preview copy. +function codebuddyContentText(content: unknown): string | null { + if (typeof content === 'string') { + return content + } + if (!Array.isArray(content)) { + return null + } + const parts = content + .map((block) => { + const record = asRecord(block) + const type = extractString(record?.type) + return type === 'input_text' || type === 'output_text' || type === 'text' + ? extractString(record?.text) + : null + }) + .filter((text): text is string => typeof text === 'string' && text.length > 0) + return parts.length > 0 ? parts.join('\n\n') : null +} + +export function consumeCodebuddySessionLine(state: CodebuddySessionParseState, line: string): void { + const { accumulator } = state + const record = parseJsonObject(line) + if (!record) { + return + } + + if (typeof record.sessionId === 'string' && record.sessionId.trim()) { + accumulator.sessionId = record.sessionId.trim() + } + updateTimeline(accumulator, record.timestamp) + updateLatestLocation(accumulator, record) + + // The CLI's own session title; later summaries revise it, like Claude's + // custom-title. + if (record.type === 'summary') { + const summary = normalizeTitleText(extractString(record.summary) ?? '') + if (summary) { + accumulator.title = summary + } + return + } + + if (record.type === 'ai-title') { + const title = normalizeTitleText(extractString(record.aiTitle) ?? '') + if (title) { + // CodeBuddy can revise generated names; AI Vault mirrors the current one. + state.aiTitle = title + } + return + } + + if (record.type !== 'message') { + return + } + + const role = extractString(record.role) + const text = codebuddyContentText(record.content) + accumulator.messageCount++ + if (role === 'user') { + // Meta prompts (injected context) only seed the last-resort title. + // `providerData.skipRun` marks machinery-injected turns that never reached + // the model as a user prompt. + const isMetaUserTurn = + asRecord(record.providerData)?.skipRun === true || + (text != null && isKnownHarnessInjectedUserTurnText(text)) + addPreviewMessage(accumulator, { + role: 'user', + text, + timestamp: record.timestamp, + seedFirstUserPrompt: !isMetaUserTurn + }) + if (text && !isMetaUserTurn) { + state.firstUserTitle ??= text + } + return + } + if (role === 'assistant') { + addPreviewMessage(accumulator, { role: 'assistant', text, timestamp: record.timestamp }) + const model = extractString(asRecord(record.providerData)?.model) + if (model) { + accumulator.model = model + } + } +} + +export function finalizeCodebuddySessionParseState( + state: CodebuddySessionParseState, + platform: NodeJS.Platform, + options: ParserSessionOptions = {} +): AiVaultSession | null { + // Finalize a snapshot: the live state (and its preview array) may keep + // accumulating appended lines after this session object is handed out. + const snapshot = cloneCodebuddySessionParseState(state) + // Why: the CLI's summary title wins via accumulator.title; the generated + // ai-title should outrank the raw first prompt when present. + snapshot.accumulator.fallbackTitle = snapshot.aiTitle ?? snapshot.firstUserTitle + return finalizeSession(snapshot.accumulator, platform, options) +} + +function codebuddyResumeStateFromParseState( + state: CodebuddySessionParseState +): ResumableSessionParseState { + return { + consumeLine: (line) => consumeCodebuddySessionLine(state, line), + identity: () => accumulatorSessionIdentity(state.accumulator), + clone: () => codebuddyResumeStateFromParseState(cloneCodebuddySessionParseState(state)), + touchFile: (file) => { + state.accumulator.modifiedAt = file.modifiedAt + }, + finalize: (platform, options) => finalizeCodebuddySessionParseState(state, platform, options) + } +} + +export function createCodebuddySessionResumeState( + file: FileWithMtime, + messages?: TranscriptMessageSink +): ResumableSessionParseState { + return codebuddyResumeStateFromParseState(createCodebuddySessionParseState(file, messages)) +} + +export async function parseCodebuddySessionFile( + file: FileWithMtime, + platform: NodeJS.Platform = process.platform, + messages?: TranscriptMessageSink +): Promise { + const lines = createInterface({ + input: openTranscriptReadStream(file.path, { encoding: 'utf-8' }, 'scan'), + crlfDelay: Infinity + }) + return parseCodebuddySessionLines({ file, lines, platform, messages }) +} + +export async function parseCodebuddySessionContent( + file: FileWithMtime, + content: RemoteSessionContent, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {}, + signal?: AbortSignal +): Promise { + return parseCodebuddySessionLines({ + file, + lines: remoteSessionContentLines(content, signal), + platform, + options + }) +} + +async function parseCodebuddySessionLines(args: { + file: FileWithMtime + lines: AsyncIterable | Iterable + platform: NodeJS.Platform + options?: ParserSessionOptions + messages?: TranscriptMessageSink +}): Promise { + const state = createCodebuddySessionParseState(args.file, args.messages) + for await (const line of args.lines) { + consumeCodebuddySessionLine(state, line) + } + return finalizeCodebuddySessionParseState(state, args.platform, args.options) +} diff --git a/src/main/ai-vault/session-scanner-codebuddy-scope.test.ts b/src/main/ai-vault/session-scanner-codebuddy-scope.test.ts new file mode 100644 index 00000000000..9086ab1e811 --- /dev/null +++ b/src/main/ai-vault/session-scanner-codebuddy-scope.test.ts @@ -0,0 +1,135 @@ +import { mkdtemp, mkdir, rm, utimes, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { scanAiVaultSessions } from './session-scanner' +import { resetProjectDirCwdCacheForTests } from './session-scanner-scope-discovery' +import { isolatedScanRoots, jsonLines } from './session-scanner-test-fixtures' + +let tempRoots: string[] = [] + +afterEach(async () => { + await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true }))) + tempRoots = [] + resetProjectDirCwdCacheForTests() +}) + +function codebuddySessionDirName(cwd: string): string { + return cwd + .replace(/[/\\:]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') +} + +async function writeCodeBuddySession(args: { + sessionsDir: string + cwd: string + id: string + timestamp: string +}): Promise { + const dir = join(args.sessionsDir, codebuddySessionDirName(args.cwd)) + await mkdir(dir, { recursive: true }) + const file = join(dir, `${args.id}.jsonl`) + await writeFile( + file, + jsonLines([ + { + type: 'message', + timestamp: args.timestamp, + sessionId: args.id, + cwd: args.cwd, + role: 'user', + content: [{ type: 'input_text', text: `prompt ${args.id}` }] + } + ]) + ) + const time = new Date(args.timestamp) + await utimes(file, time, time) +} + +describe('scanAiVaultSessions — CodeBuddy scope discovery', () => { + it('lists an older in-scope CodeBuddy session the recency cap would drop', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-codebuddy-scope-')) + tempRoots.push(root) + const roots = isolatedScanRoots(root) + const workspace = '/home/ada/orca/workspaces/orca/feature' + + await writeCodeBuddySession({ + sessionsDir: roots.codebuddyProjectsDir, + cwd: `${workspace}/packages/app`, + id: 'old-in-scope', + timestamp: '2026-05-01T10:00:00.000Z' + }) + // Newer, out of scope, and a sibling whose encoding shares the workspace prefix. + await writeCodeBuddySession({ + sessionsDir: roots.codebuddyProjectsDir, + cwd: '/home/ada/other', + id: 'recent-elsewhere', + timestamp: '2026-06-01T10:00:00.000Z' + }) + await writeCodeBuddySession({ + sessionsDir: roots.codebuddyProjectsDir, + cwd: `${workspace}-sibling`, + id: 'sibling', + timestamp: '2026-04-01T10:00:00.000Z' + }) + + const result = await scanAiVaultSessions({ + ...roots, + platform: 'linux', + limit: 1, + scopePaths: [workspace] + }) + const ids = result.sessions.map((session) => session.sessionId) + + expect(ids).toContain('old-in-scope') + expect(ids).toContain('recent-elsewhere') + expect(ids).not.toContain('sibling') + expect(result.sessions.find((session) => session.sessionId === 'old-in-scope')?.agent).toBe( + 'codebuddy' + ) + }) + + it('finds an older session when another cwd shares its encoded bucket', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-codebuddy-collision-')) + tempRoots.push(root) + const roots = isolatedScanRoots(root) + for (const [cwd, id, timestamp] of [ + ['/home/ada/repo/app', 'in-scope', '2026-04-01T10:00:00.000Z'], + ['/home/ada/repo-app', 'collision', '2026-05-01T10:00:00.000Z'], + ['/home/ada/elsewhere', 'recent', '2026-06-01T10:00:00.000Z'] + ]) { + await writeCodeBuddySession({ sessionsDir: roots.codebuddyProjectsDir, cwd, id, timestamp }) + } + const result = await scanAiVaultSessions({ + ...roots, + platform: 'linux', + limit: 1, + scopePaths: ['/home/ada/repo/app'] + }) + expect(result.sessions.map((session) => session.sessionId)).toContain('in-scope') + expect(result.sessions.map((session) => session.sessionId)).not.toContain('collision') + }) + + it('adds nothing when no scope is requested', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-codebuddy-noscope-')) + tempRoots.push(root) + const roots = isolatedScanRoots(root) + await writeCodeBuddySession({ + sessionsDir: roots.codebuddyProjectsDir, + cwd: '/home/ada/old', + id: 'old', + timestamp: '2026-05-01T10:00:00.000Z' + }) + await writeCodeBuddySession({ + sessionsDir: roots.codebuddyProjectsDir, + cwd: '/home/ada/new', + id: 'new', + timestamp: '2026-06-01T10:00:00.000Z' + }) + + const result = await scanAiVaultSessions({ ...roots, platform: 'linux', limit: 1 }) + + expect(result.sessions.map((session) => session.sessionId)).toEqual(['new']) + }) +}) diff --git a/src/main/ai-vault/session-scanner-codebuddy-source.test.ts b/src/main/ai-vault/session-scanner-codebuddy-source.test.ts new file mode 100644 index 00000000000..a171ac6d671 --- /dev/null +++ b/src/main/ai-vault/session-scanner-codebuddy-source.test.ts @@ -0,0 +1,80 @@ +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { scanAiVaultSessions } from './session-scanner' +import { isolatedScanRoots, jsonLines } from './session-scanner-test-fixtures' + +let tempRoots: string[] = [] + +afterEach(async () => { + await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true }))) + tempRoots = [] +}) + +describe('scanAiVaultSessions — CodeBuddy transcripts', () => { + it('discovers ~/.codebuddy/projects transcripts and attributes them to codebuddy', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-codebuddy-')) + tempRoots.push(root) + const roots = isolatedScanRoots(root) + await mkdir(join(roots.codebuddyProjectsDir, 'project'), { recursive: true }) + + await writeFile( + join(roots.codebuddyProjectsDir, 'project', 'codebuddy-session.jsonl'), + jsonLines([ + { + id: 'cb-msg-1', + timestamp: 1_777_634_400_000, + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'fix the flaky test' }], + sessionId: 'codebuddy-session', + cwd: '/repo/app' + }, + { + id: 'cb-msg-2', + timestamp: 1_777_634_405_000, + type: 'message', + role: 'assistant', + status: 'completed', + content: [{ type: 'output_text', text: 'On it' }], + providerData: { model: 'glm-5.3-flash' }, + sessionId: 'codebuddy-session', + cwd: '/repo/app' + }, + { + id: 'cb-title-1', + timestamp: 1_777_634_406_000, + type: 'ai-title', + aiTitle: 'Fixing the flaky test', + sessionId: 'codebuddy-session', + cwd: '/repo/app' + }, + { + id: 'cb-summary-1', + timestamp: 1_777_634_407_000, + type: 'summary', + summary: 'Fix the flaky test', + providerData: { source: 'initial-user-message' } + } + ]) + ) + + const result = await scanAiVaultSessions({ ...roots, platform: 'darwin' }) + + const codebuddy = result.sessions.find((session) => session.agent === 'codebuddy') + expect(codebuddy).toMatchObject({ + sessionId: 'codebuddy-session', + cwd: '/repo/app', + title: 'Fix the flaky test', + model: 'glm-5.3-flash', + resumeCommand: "cd '/repo/app' && codebuddy --resume 'codebuddy-session'", + filePath: join(roots.codebuddyProjectsDir, 'project', 'codebuddy-session.jsonl') + }) + expect(codebuddy?.messageCount).toBe(2) + expect(codebuddy?.previewMessages.map((message) => message.text)).toEqual([ + 'fix the flaky test', + 'On it' + ]) + }) +}) diff --git a/src/main/ai-vault/session-scanner-log-agent-fixtures.ts b/src/main/ai-vault/session-scanner-log-agent-fixtures.ts index ed2cf8d2eee..be080271269 100644 --- a/src/main/ai-vault/session-scanner-log-agent-fixtures.ts +++ b/src/main/ai-vault/session-scanner-log-agent-fixtures.ts @@ -32,6 +32,23 @@ export async function writeLogAgentFixtures( ]) ) + // CodeBuddy writes its own message-record shape under its own root. + await mkdir(join(roots.codebuddyProjectsDir, 'project'), { recursive: true }) + await writeFile( + join(roots.codebuddyProjectsDir, 'project', 'codebuddy-session.jsonl'), + jsonlBody([ + { + id: 'codebuddy-msg-1', + timestamp: 1_777_634_430_000, + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'CodeBuddy title' }], + sessionId: 'codebuddy-session', + cwd: '/tmp/codebuddy' + } + ]) + ) + await mkdir(join(roots.codexSessionsDir, '2026', '05', '01'), { recursive: true }) await writeFile( join(roots.codexSessionsDir, '2026', '05', '01', 'rollout-2026-codex-session.jsonl'), diff --git a/src/main/ai-vault/session-scanner-parse-cache.ts b/src/main/ai-vault/session-scanner-parse-cache.ts index 4610c198cb3..a00f4edd2cf 100644 --- a/src/main/ai-vault/session-scanner-parse-cache.ts +++ b/src/main/ai-vault/session-scanner-parse-cache.ts @@ -5,6 +5,7 @@ import { createCodexSessionResumeState } from './session-scanner-codex-parser' import { createDroidSessionResumeState } from './session-scanner-droid-parser' import { createMessageGraphSessionResumeState } from './session-scanner-graph-parsers' import { createClaudeSessionResumeState } from './session-scanner-primary-parsers' +import { createCodebuddySessionResumeState } from './session-scanner-codebuddy-parser' import { createGeminiJsonlSessionResumeState } from './session-scanner-gemini-parsers' import { createCopilotSessionResumeState } from './session-scanner-copilot-parser' import { createCursorSessionResumeState } from './session-scanner-cursor-parser' @@ -51,6 +52,8 @@ function resumableStateFactoryFor( switch (candidate.agent) { case 'claude': return (messages) => createClaudeSessionResumeState(candidate.file, messages) + case 'codebuddy': + return (messages) => createCodebuddySessionResumeState(candidate.file, messages) case 'codex': return (messages) => createCodexSessionResumeState(candidate.file, candidate.codexHome, messages) diff --git a/src/main/ai-vault/session-scanner-scope-discovery.ts b/src/main/ai-vault/session-scanner-scope-discovery.ts index d9ef112f772..52795830e66 100644 --- a/src/main/ai-vault/session-scanner-scope-discovery.ts +++ b/src/main/ai-vault/session-scanner-scope-discovery.ts @@ -20,7 +20,7 @@ const REPRESENTATIVE_CWD_LINE_LIMIT = 200 const REPRESENTATIVE_FILE_LIMIT = 3 const TRANSCRIPT_EXTENSIONS = new Set(['.jsonl']) -// Both encodings are lossy; retain Claude’s existing cache and verify each Pi transcript. +// Bucket encodings are lossy; retain Claude’s cache and verify other transcripts individually. const PROJECT_DIR_CWD_CACHE_MAX = 2048 const projectDirCwdCache = new Map() @@ -75,7 +75,7 @@ type InScopeDiscoveryArgs = { } /** - * Fully include the transcripts of cwd-bucket directories (Claude, Pi) whose cwd + * Fully include the transcripts of cwd-bucket directories (Claude, Pi, CodeBuddy) whose cwd * falls inside the active workspace/project paths. * * Why: these agents keep one directory per cwd, e.g. `~/.claude/projects//`. @@ -108,7 +108,7 @@ export async function discoverInScopeCwdBucketFiles( } await collectBucketFiles({ agent: layout.agent, - scopePaths: layout.agent === 'pi' ? args.scopePaths : undefined, + scopePaths: layout.agent !== 'claude' ? args.scopePaths : undefined, projectDir, issues: args.issues, collected, diff --git a/src/main/ai-vault/session-scanner-test-fixtures.ts b/src/main/ai-vault/session-scanner-test-fixtures.ts index 43d03d340dd..0d9b8cef5ab 100644 --- a/src/main/ai-vault/session-scanner-test-fixtures.ts +++ b/src/main/ai-vault/session-scanner-test-fixtures.ts @@ -54,6 +54,7 @@ export async function writeOpenCode2SqliteFixture(root: string): Promise export function isolatedScanRoots(root: string) { return { claudeProjectsDir: join(root, 'claude-projects'), + codebuddyProjectsDir: join(root, 'codebuddy-projects'), codexSessionsDir: join(root, 'codex-sessions'), geminiSessionsDir: join(root, 'gemini-sessions'), antigravityBrainDir: join(root, 'antigravity-brain'), diff --git a/src/main/ai-vault/session-scanner-types.ts b/src/main/ai-vault/session-scanner-types.ts index 099b3593431..be97a4b3187 100644 --- a/src/main/ai-vault/session-scanner-types.ts +++ b/src/main/ai-vault/session-scanner-types.ts @@ -14,6 +14,7 @@ import type { OpenCodeWslRuntime } from './session-scanner-opencode-wsl-runtime' export type AiVaultScanOptions = { claudeProjectsDir?: string + codebuddyProjectsDir?: string codexSessionsDir?: string additionalCodexSessionsDirs?: readonly string[] // Why: tests inject a sandbox "real ~/.codex" so real-home attribution diff --git a/src/main/ai-vault/session-scanner.ts b/src/main/ai-vault/session-scanner.ts index 2f5629f94c3..f56f17af796 100644 --- a/src/main/ai-vault/session-scanner.ts +++ b/src/main/ai-vault/session-scanner.ts @@ -27,7 +27,11 @@ import { getSessionParseCacheEntry } from './session-parse-cache-store' import { describeSkippedTranscriptRecords } from './session-transcript-record-budget' import { canStopParsingSessions } from './session-scan-cutoff' import { discoverInScopeCwdBucketFiles } from './session-scanner-scope-discovery' -import { CLAUDE_CWD_BUCKET_LAYOUT, PI_CWD_BUCKET_LAYOUT } from './session-cwd-bucket-layouts' +import { + CLAUDE_CWD_BUCKET_LAYOUT, + PI_CWD_BUCKET_LAYOUT, + CODEBUDDY_CWD_BUCKET_LAYOUT +} from './session-cwd-bucket-layouts' import { discoverAiVaultSessionSources } from './session-scanner-source-discovery' import { cursorChatMetaRefusals, withCursorChatMetaScan } from './session-scanner-cursor-chat-meta' import type { @@ -166,7 +170,11 @@ function mergeSessions( // Agents whose on-disk layout names a directory per cwd, so a scope's older // sessions can be found without reading every transcript's header. -const CWD_BUCKET_LAYOUTS = [CLAUDE_CWD_BUCKET_LAYOUT, PI_CWD_BUCKET_LAYOUT] +const CWD_BUCKET_LAYOUTS = [ + CLAUDE_CWD_BUCKET_LAYOUT, + PI_CWD_BUCKET_LAYOUT, + CODEBUDDY_CWD_BUCKET_LAYOUT +] async function scanInScopeSessions(args: { discoveries: SessionFileDiscovery[] diff --git a/src/main/browser/browser-manager-auth-user-agent.test.ts b/src/main/browser/browser-manager-auth-user-agent.test.ts index d85a785a887..1a24369f58c 100644 --- a/src/main/browser/browser-manager-auth-user-agent.test.ts +++ b/src/main/browser/browser-manager-auth-user-agent.test.ts @@ -137,10 +137,22 @@ describe('browserManager', () => { { userAgent: guestBaseUserAgent } ]) - // A navigation that doesn't change the required UA must not thrash setUserAgent. + // The next direct navigation is the first safe moment to rewrite the WebContents UA, so it + // restores the process identity there and drops the CDP override that stood in for it. setUserAgent.mockClear() + sendCommand.mockClear() didStartNavigation(null, 'https://example.com/', false, true) + expect(setUserAgent).toHaveBeenCalledOnce() + expect(setUserAgent).toHaveBeenLastCalledWith(guestBaseUserAgent) + expect(sendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { userAgent: '' }) + await expect(sendCommand.mock.results.at(-1)?.value).resolves.toBeUndefined() + + // A navigation that doesn't change the required UA must not thrash either layer. + setUserAgent.mockClear() + sendCommand.mockClear() + didStartNavigation(null, 'https://example.org/', false, true) expect(setUserAgent).not.toHaveBeenCalled() + expect(sendCommand).not.toHaveBeenCalled() }) it('leaves the UA untouched on Google auth hosts in native process mode', () => { @@ -270,7 +282,8 @@ describe('browserManager', () => { // Why: the CDP override outranks the WebContents UA, so getUserAgent() still reports the base // identity. Leaving the auth host must read the override, not that stale value, or the guest - // keeps presenting Firefox on every later host. + // keeps presenting Firefox on every later host. The WebContents UA already presents the process + // identity, so leaving clears the override rather than restating the UA without client hints. sendCommand.mockClear() const didStartNavigation = guestOnMock.mock.calls.find( ([event]) => event === 'did-start-navigation' @@ -282,8 +295,8 @@ describe('browserManager', () => { expect(guest.setUserAgent).not.toHaveBeenCalled() expect(sendCommand.mock.calls).toEqual([ - ['Emulation.setUserAgentOverride', { userAgent: guestBaseUserAgent }], - ['Emulation.setUserAgentOverride', { userAgent: guestBaseUserAgent }] + ['Emulation.setUserAgentOverride', { userAgent: '' }], + ['Emulation.setUserAgentOverride', { userAgent: '' }] ]) // A newer confirmed write outranks an older write that remains in flight. @@ -330,9 +343,7 @@ describe('browserManager', () => { sendCommand.mockClear() didStartNavigation(null, 'https://example.com/', false, true) - expect(sendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: guestBaseUserAgent - }) + expect(sendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { userAgent: '' }) browserManager.unregisterGuest('browser-redirect-ua') }) @@ -420,14 +431,14 @@ describe('browserManager', () => { ) }) - // Why: the direct-navigation branch still writes the Firefox UA through WebContents.setUserAgent, - // and nothing ever restores it once the guest switches to the CDP override. A viewport preset that - // read getUserAgent() back as its base identity would therefore republish Firefox on every ordinary - // host — the wire UA saying Firefox while sec-ch-ua still says Chrome, the exact cross-layer tell - // this scope exists to remove. - it('keeps a viewport preset on the session identity after an auth-host visit', async () => { + // Why: a direct navigation onto the auth host writes Firefox into the WebContents UA, and a redirect + // off it cannot rewrite that layer without cancelling the navigation. Clearing the CDP override + // there would expose that stale Firefox UA on an ordinary host, so the override must carry the + // process identity until the next direct navigation can restore the WebContents UA and clear it. + it('never clears the override onto a stale Firefox WebContents UA and heals it on the next navigation', async () => { browserMocks.processUserAgent = GUEST_CLEAN_UA - const { guest, debuggerSendCommand } = makeViewportGuest(9001) + const { guest, debuggerSendCommand, presentedUserAgent, standingUserAgentOverride } = + makeViewportGuest(9001) webContentsFromIdMock.mockReturnValue(guest) browserManager.attachGuestPolicies(guest as never) browserManager.registerGuest({ @@ -452,22 +463,23 @@ describe('browserManager', () => { didStartNavigation(null, 'https://accounts.google.com/v3/signin/identifier', false, true) await flushViewportOps() - // The direct branch pins the WebContents UA to Firefox and never restores it. expect((guest.getUserAgent as () => string)()).toBe(googleAuthUserAgent()) + debuggerSendCommand.mockClear() willRedirect({ preventDefault: vi.fn() }, 'https://myaccount.google.com/', false, true) await flushViewportOps() + expect(debuggerSendCommand).toHaveBeenLastCalledWith('Emulation.setUserAgentOverride', { + userAgent: GUEST_CLEAN_UA + }) + expect(presentedUserAgent()).toBe(GUEST_CLEAN_UA) debuggerSendCommand.mockClear() didStartNavigation(null, 'https://github.com/', false, true) await flushViewportOps() - - const uaWrites = debuggerSendCommand.mock.calls.filter( - ([method]) => method === 'Emulation.setUserAgentOverride' - ) - expect(uaWrites.length).toBeGreaterThan(0) - for (const [, params] of uaWrites) { - expect((params as { userAgent: string }).userAgent).toBe(GUEST_CLEAN_UA) - } + expect(debuggerSendCommand).toHaveBeenLastCalledWith('Emulation.setUserAgentOverride', { + userAgent: '' + }) + expect(standingUserAgentOverride()).toBeNull() + expect(presentedUserAgent()).toBe(GUEST_CLEAN_UA) }) }) diff --git a/src/main/browser/browser-manager-bindings.ts b/src/main/browser/browser-manager-bindings.ts index 931c5c6bc3b..a87c7e73e08 100644 --- a/src/main/browser/browser-manager-bindings.ts +++ b/src/main/browser/browser-manager-bindings.ts @@ -77,8 +77,8 @@ export abstract class BrowserManagerBindings extends BrowserManagerGrab { resolveRenderer: (tabId) => resolveRendererWebContents(this.rendererWebContentsIdByTabId, tabId), isViewportPresetActive: () => { - const state = this.viewportPresetActiveByTabId.get(browserTabId) - return state?.guestWebContentsId === guest.id && state.active + const state = this.viewportPresetByTabId.get(browserTabId) + return state?.guestWebContentsId === guest.id && state.override !== null }, canViewportScroll: (mouse) => this.canViewportScroll(browserTabId, mouse), onViewportWheelConsumed: (deltaX, deltaY) => diff --git a/src/main/browser/browser-manager-guest-cleanup.ts b/src/main/browser/browser-manager-guest-cleanup.ts index a815e95e431..67ded30cecc 100644 --- a/src/main/browser/browser-manager-guest-cleanup.ts +++ b/src/main/browser/browser-manager-guest-cleanup.ts @@ -23,7 +23,7 @@ export abstract class BrowserManagerGuestCleanup extends BrowserManagerGuestNavi this.offscreenGuestIds.delete(guestWebContentsId) this.popupOwnerContextByGuestId.delete(guestWebContentsId) this.pageInitiatedTabBudgetByRootGuestId.delete(guestWebContentsId) - this.authUserAgentOverrideStateByGuestId.delete(guestWebContentsId) + this.cdpUserAgentOverrideStateByGuestId.delete(guestWebContentsId) this.pendingNavigationByGuestId.delete(guestWebContentsId) // Why: a popup must stop inheriting authorization the moment its owner retires, before Chromium destroys the child. if (isPrimaryGuest) { diff --git a/src/main/browser/browser-manager-guest-navigation-policy.ts b/src/main/browser/browser-manager-guest-navigation-policy.ts index a2b8bf0b329..9e8a301476f 100644 --- a/src/main/browser/browser-manager-guest-navigation-policy.ts +++ b/src/main/browser/browser-manager-guest-navigation-policy.ts @@ -35,7 +35,7 @@ export abstract class BrowserManagerGuestNavigationPolicy extends BrowserManager return } this.updatePendingNavigationForRedirect(guest.id, url) - this.applyGoogleAuthUserAgent(guest, url, { duringRedirect: true }) + void this.retargetTabIdentity(guest, url) } const didFailLoadHandler = ( @@ -51,8 +51,8 @@ export abstract class BrowserManagerGuestNavigationPolicy extends BrowserManager // Why: a nav that never committed must not leave its target standing as the tab's host. const failedNavigationWasCurrent = this.failPendingNavigation(guest.id, validatedURL) if (failedNavigationWasCurrent) { - // The attempted host never committed, so restore every UA layer to the document that remains. - this.applyGoogleAuthUserAgent(guest, guest.getURL()) + // Restore the identity of the document that remains (usually the failed URL's error page). + void this.retargetTabIdentity(guest, guest.getURL()) } const browserPageId = this.tabIdByWebContentsId.get(guest.id) const certificateFailure = browserPageId @@ -91,7 +91,7 @@ export abstract class BrowserManagerGuestNavigationPolicy extends BrowserManager const didStartNavigationHandler = ( _event: Electron.Event, url: string, - _isInPlace: boolean, + isInPlace: boolean, isMainFrame: boolean ): void => { if (!isMainFrame || isChromiumInternalErrorUrl(url)) { @@ -100,7 +100,7 @@ export abstract class BrowserManagerGuestNavigationPolicy extends BrowserManager // Why: getURL() still reports the previous committed URL until this navigation commits, so // every UA writer must read the in-flight target or they disagree about the tab's host. this.startPendingNavigation(guest.id, url) - this.applyGoogleAuthUserAgent(guest, url) + void this.presentTabIdentityAtNavigationStart(guest, url, isInPlace) this.certificateTrustController?.onMainFrameNavigationStarted(guest.id) // Why: a pre-registration failure belongs only to its own nav; a replacement nav must not replay it. this.pendingLoadFailuresByGuestId.delete(guest.id) diff --git a/src/main/browser/browser-manager-guest-policy.ts b/src/main/browser/browser-manager-guest-policy.ts index dd418d71af7..9c99175096f 100644 --- a/src/main/browser/browser-manager-guest-policy.ts +++ b/src/main/browser/browser-manager-guest-policy.ts @@ -26,7 +26,7 @@ export abstract class BrowserManagerGuestPolicy extends BrowserManagerGuestClean if (inheritedOwnerContext) { this.popupOwnerContextByGuestId.set(guest.id, inheritedOwnerContext) } - const disposeAuthDetachTracking = this.trackDebuggerDetachForAuthUserAgent(guest) + const disposeDetachTracking = this.trackDebuggerDetachForUserAgentOverride(guest) // Why: disable throttling so background screenshots still get frames; else the compositor stalls and capture returns empty. guest.setBackgroundThrottling(false) const disposePopupPolicy = this.installGuestPopupPolicy(guest, !inheritedOwnerContext) @@ -34,7 +34,7 @@ export abstract class BrowserManagerGuestPolicy extends BrowserManagerGuestClean // Why: store cleanup so unregisterGuest can drop these listeners on teardown and let the WebContents wrapper GC. this.policyCleanupByGuestId.set(guest.id, () => { - disposeAuthDetachTracking() + disposeDetachTracking() disposePopupPolicy() disposeNavigationPolicy() }) diff --git a/src/main/browser/browser-manager-navigation.ts b/src/main/browser/browser-manager-navigation.ts index a7dcf5cd4fc..d96d41e79be 100644 --- a/src/main/browser/browser-manager-navigation.ts +++ b/src/main/browser/browser-manager-navigation.ts @@ -1,145 +1,152 @@ import { openPopupWithOriginBar, type PopupChildWindowOptions } from './popup-origin-bar-window' import { getBrowserProcessUserAgentIdentity } from './browser-process-user-agent' import type { BrowserSessionRequestUserAgentResolver } from './browser-session-ua' -import { googleAuthUserAgent, isGoogleAuthUrl } from './browser-google-auth-ua' +import { isGoogleAuthUrl } from './browser-google-auth-ua' import { - buildViewportUserAgentOverride, - type ViewportUserAgentOverride -} from './browser-viewport-user-agent' + googleAuthTabIdentity, + resolveBrowserTabIdentity, + type BrowserTabIdentity +} from './browser-tab-identity' import { safeOrigin, - type AuthUserAgentOverrideOperation, - type AuthUserAgentOverrideState + type CdpUserAgentOverride, + type CdpUserAgentOverrideOperation, + type CdpUserAgentOverrideState } from './browser-manager-types' import { BrowserManagerVisibility } from './browser-manager-visibility' export abstract class BrowserManagerNavigation extends BrowserManagerVisibility { resolveBrowserGuestRequestUserAgent( request: Parameters[0] - ): ViewportUserAgentOverride { - const identity = getBrowserProcessUserAgentIdentity() - const firefoxUa = googleAuthUserAgent() + ): BrowserTabIdentity { + const processIdentity = getBrowserProcessUserAgentIdentity() + const googleAuth = googleAuthTabIdentity() const pendingNavigation = request.webContentsId === undefined ? undefined : this.pendingNavigationByGuestId.get(request.webContentsId) // Firefox is delivered per-target and cannot reach workers; keep it clean-only to preserve one // coherent identity per mode instead of pairing a Firefox document with native workers. - const googleAuthEnabled = identity.mode === 'clean' + const googleAuthEnabled = processIdentity.mode === 'clean' if ( googleAuthEnabled && - request.currentUserAgent === firefoxUa && + request.currentUserAgent === googleAuth.userAgent && (!pendingNavigation || isGoogleAuthUrl(pendingNavigation.currentUrl)) ) { - return { userAgent: firefoxUa } + return googleAuth } - const overrideState = + const standingOverride = request.webContentsId === undefined ? undefined - : this.authUserAgentOverrideStateByGuestId.get(request.webContentsId) - const latestPendingOverride = overrideState?.pending.at(-1) - const currentOverride = - latestPendingOverride && - latestPendingOverride.sequence > (overrideState?.confirmed?.sequence ?? -1) - ? latestPendingOverride - : overrideState?.confirmed + : this.standingCdpUserAgentOverride(request.webContentsId) if ( googleAuthEnabled && - !currentOverride && - request.effectiveUserAgent === firefoxUa && + !standingOverride && + request.effectiveUserAgent === googleAuth.userAgent && (!pendingNavigation || isGoogleAuthUrl(pendingNavigation.currentUrl)) ) { - return { userAgent: firefoxUa } + return googleAuth } - if (googleAuthEnabled && currentOverride?.userAgent === firefoxUa) { - return { userAgent: firefoxUa } + if (googleAuthEnabled && standingOverride?.userAgent === googleAuth.userAgent) { + return googleAuth } - const browserPageId = - request.webContentsId === undefined - ? undefined - : this.tabIdByWebContentsId.get(request.webContentsId) // Shared and service worker requests carry no webContentsId, and resolving a session-wide mobile // intent for one put the mobile UA on the wire for a context whose own navigator.userAgent is // desktop-clean — and for every tab sharing the session. One context, one identity: those workers // stay on the session identity, while emulation reaches documents and the emulated tab's dedicated - // workers, which carry the owning webContentsId and so resolve through browserPageId. - const mobile = browserPageId - ? (this.viewportUaOverrideMobileByTabId.get(browserPageId) ?? false) - : false - return buildViewportUserAgentOverride({ + // workers, which carry the owning webContentsId. + // Why the standing override and not the requested preset: the wire must match what the document + // presents, and a preset whose CDP write never landed (no debugger, a failed write) presents none. + return resolveBrowserTabIdentity({ url: request.url, - mobile, - baseUserAgent: identity.userAgent, - googleAuthEnabled + mobile: standingOverride?.userAgentMetadata?.mobile === true, + processIdentity }) } - // Why: navigator.userAgent (read by Google's auth JS) reflects the WebContents UA, - // not the request header, so the Firefox switch in the session request hook - // must be matched here per navigation or the two layers disagree — itself a bot tell. - protected applyGoogleAuthUserAgent( + // Why: gate on the DIRECT page id, not ownerTabId — a popup has no device-metrics override of its + // own, so inheriting the owner tab's preset would pair a mobile UA with a desktop viewport. + protected hasMobileViewportPreset(guestWebContentsId: number): boolean { + const browserPageId = this.tabIdByWebContentsId.get(guestWebContentsId) + const preset = browserPageId ? this.viewportPresetByTabId.get(browserPageId) : undefined + return preset?.guestWebContentsId === guestWebContentsId && preset.override?.mobile === true + } + + /** + * The only writer of the WebContents UA. Chromium cancels a redirect, and reloads a loading + * document, when that UA changes at any other moment; a cross-document did-start-navigation is + * the one point where it lands on the new document alone. Call it synchronously from that event. + */ + protected presentTabIdentityAtNavigationStart( guest: Electron.WebContents, url: string, - options: { duringRedirect?: boolean } = {} - ): void { - const browserPageId = this.tabIdByWebContentsId.get(guest.id) - const identity = getBrowserProcessUserAgentIdentity() - if (identity.mode === 'native') { - if (browserPageId) { - this.reapplyViewportUserAgentOverride(guest, browserPageId, url) - } - return - } - const firefoxUa = googleAuthUserAgent() - const overrideState = this.authUserAgentOverrideStateByGuestId.get(guest.id) - const latestPendingOverride = overrideState?.pending.at(-1) - const confirmedOverride = overrideState?.confirmed - const currentOverride = - latestPendingOverride && latestPendingOverride.sequence > (confirmedOverride?.sequence ?? -1) - ? latestPendingOverride - : confirmedOverride - const currentUa = currentOverride?.userAgent ?? guest.getUserAgent() - const nextUa = isGoogleAuthUrl(url) - ? firefoxUa - : // Only restore when the auth-host override is actually in place, so normal - // navigation never touches the session UA. - currentUa === firefoxUa + sameDocument: boolean + ): Promise { + const identity = this.resolveGuestTabIdentity(guest, url) + // The WebContents UA carries no mobile identity: that needs metadata only CDP can send. + const webContentsUserAgent = + identity.kind === 'google-auth' ? identity.userAgent - : null - let authOverrideIssuedOverCdp = false - if (nextUa !== null && nextUa !== currentUa) { - // Why: WebContents.setUserAgent() during a redirect makes Chromium cancel the in-flight - // navigation (ERR_ABORTED) and replay the original request, which a POST-started OAuth chain - // cannot survive — the sign-in lands on a blank tab. CDP retargets navigator.userAgent without - // touching the navigation, and it outranks the WebContents UA from then on, so a guest that - // switches to it stays on it. The wire UA never depended on this write: - // The session request hook rewrites User-Agent for auth-host URLs on its own. - if (options.duringRedirect === true || overrideState !== undefined) { - if (this.canOverrideUserAgentOverCdp(guest)) { - authOverrideIssuedOverCdp = true - // Why: go through the viewport builder rather than writing nextUa raw, so both CDP writers - // resolve one identity for this URL — Firefox on auth hosts, the profile's clean base off - // them, any mobile preset preserved. Writing the session UA directly would put the - // unlaundered Electron token back on the wire. - void this.applyAuthUserAgentOverrideOverCdp( - guest, - (browserPageId ? this.viewportUaOverrideMobileByTabId.get(browserPageId) : undefined) ?? - false, - url, - nextUa - ) - } - // Why: with no debugger there is no way to retarget the identity without cancelling the - // redirect. A stale navigator.userAgent is recoverable; a dead navigation is not. - } else { - guest.setUserAgent(nextUa) - } + : getBrowserProcessUserAgentIdentity().userAgent + let presentedByWebContents = guest.getUserAgent() + if (!sameDocument && presentedByWebContents !== webContentsUserAgent) { + guest.setUserAgent(webContentsUserAgent) + presentedByWebContents = webContentsUserAgent } - // Why: gate on the DIRECT page id, not ownerTabId — a popup has no device-metrics override of - // its own, so inheriting the owner tab's preset UA would pair a mobile UA with a desktop viewport. - if (browserPageId && !authOverrideIssuedOverCdp) { - this.reapplyViewportUserAgentOverride(guest, browserPageId, url) + return this.writeTabIdentityOverride(guest, identity, presentedByWebContents) + } + + /** Every other identity change (redirects, failed loads, preset changes) goes over CDP only. */ + protected retargetTabIdentity(guest: Electron.WebContents, url: string): Promise { + return this.writeTabIdentityOverride( + guest, + this.resolveGuestTabIdentity(guest, url), + guest.getUserAgent() + ) + } + + private resolveGuestTabIdentity(guest: Electron.WebContents, url: string): BrowserTabIdentity { + return resolveBrowserTabIdentity({ + url, + mobile: this.hasMobileViewportPreset(guest.id), + processIdentity: getBrowserProcessUserAgentIdentity() + }) + } + + // Why clear rather than restate: any override without userAgentMetadata makes Chromium drop + // navigator.userAgentData and every sec-ch-ua header, so it stands only when the WebContents UA + // cannot present the identity itself. + private writeTabIdentityOverride( + guest: Electron.WebContents, + identity: BrowserTabIdentity, + presentedByWebContents: string + ): Promise { + const override: CdpUserAgentOverride = + identity.kind === 'mobile' + ? { userAgent: identity.userAgent, userAgentMetadata: identity.userAgentMetadata } + : presentedByWebContents === identity.userAgent + ? { userAgent: '' } + : // Only reachable before a navigation start can rewrite the WebContents UA (a redirect + // off the auth host, a failed load); the next cross-document navigation clears it. + { userAgent: identity.userAgent } + if (override.userAgent === '' && !this.standingCdpUserAgentOverride(guest.id)) { + return Promise.resolve(true) } + // Why: with no debugger there is no way to retarget the identity without cancelling a redirect. + // A stale navigator.userAgent is recoverable; a dead navigation is not. The wire UA never + // depended on this write: the session request hook rewrites User-Agent on its own. + return this.writeCdpUserAgentOverride(guest, override) + } + + /** The override Chromium holds or is about to hold; undefined when none stands. */ + protected standingCdpUserAgentOverride(guestId: number): CdpUserAgentOverride | undefined { + const state = this.cdpUserAgentOverrideStateByGuestId.get(guestId) + const latestPending = state?.pending.at(-1) + const current = + latestPending && latestPending.sequence > (state?.confirmed?.sequence ?? -1) + ? latestPending + : state?.confirmed + return current && current.override.userAgent !== '' ? current.override : undefined } protected canOverrideUserAgentOverCdp(guest: Electron.WebContents): boolean { @@ -150,39 +157,43 @@ export abstract class BrowserManagerNavigation extends BrowserManagerVisibility } } - protected applyAuthUserAgentOverrideOverCdp( + // Why no queue: debugger.sendCommand dispatches in call order over one channel, so the later-issued + // write wins. The sequence only keeps a failed or superseded write from being recorded as standing. + protected writeCdpUserAgentOverride( guest: Electron.WebContents, - mobile: boolean, - url: string, - userAgent: string + override: CdpUserAgentOverride ): Promise { if (!this.canOverrideUserAgentOverCdp(guest)) { return Promise.resolve(false) } - const state = this.authUserAgentOverrideStateByGuestId.get(guest.id) ?? { + const state = this.cdpUserAgentOverrideStateByGuestId.get(guest.id) ?? { confirmed: null, nextSequence: 0, pending: [] } - const operation = { sequence: ++state.nextSequence, userAgent } + const operation = { sequence: ++state.nextSequence, override } state.pending.push(operation) - this.authUserAgentOverrideStateByGuestId.set(guest.id, state) - return this.sendViewportUserAgentOverride(guest, mobile, url, userAgent).then( - () => this.settleAuthUserAgentOverride(guest.id, state, operation, true), - () => { - this.settleAuthUserAgentOverride(guest.id, state, operation, false) + this.cdpUserAgentOverrideStateByGuestId.set(guest.id, state) + return guest.debugger.sendCommand('Emulation.setUserAgentOverride', override).then( + () => this.settleCdpUserAgentOverride(guest.id, state, operation, true), + (error: unknown) => { + this.settleCdpUserAgentOverride(guest.id, state, operation, false) + console.warn('[browser-manager] failed to write the tab user agent override', { + guestWebContentsId: guest.id, + error: error instanceof Error ? error.message : String(error) + }) return false } ) } - protected settleAuthUserAgentOverride( + protected settleCdpUserAgentOverride( guestId: number, - state: AuthUserAgentOverrideState, - operation: AuthUserAgentOverrideOperation, + state: CdpUserAgentOverrideState, + operation: CdpUserAgentOverrideOperation, succeeded: boolean ): boolean { - if (this.authUserAgentOverrideStateByGuestId.get(guestId) !== state) { + if (this.cdpUserAgentOverrideStateByGuestId.get(guestId) !== state) { return false } if (succeeded && (state.confirmed?.sequence ?? -1) < operation.sequence) { @@ -192,10 +203,13 @@ export abstract class BrowserManagerNavigation extends BrowserManagerVisibility if (pendingIndex !== -1) { state.pending.splice(pendingIndex, 1) } - if (state.confirmed === null && state.pending.length === 0) { - this.authUserAgentOverrideStateByGuestId.delete(guestId) + if ( + (state.confirmed === null || state.confirmed.override.userAgent === '') && + state.pending.length === 0 + ) { + this.cdpUserAgentOverrideStateByGuestId.delete(guestId) } - return true + return succeeded } protected startPendingNavigation(guestId: number, url: string): void { @@ -242,48 +256,6 @@ export abstract class BrowserManagerNavigation extends BrowserManagerVisibility return this.pendingNavigationByGuestId.get(guest.id)?.currentUrl ?? guest.getURL() } - // Why: Emulation.setUserAgentOverride is set once and stands across every later navigation, - // outranking setUserAgent for navigator.userAgent. A viewport preset applied before reaching an - // auth host would otherwise pin navigator.userAgent to the Chrome-shaped preset UA while the - // request header says Firefox — the two-layer disagreement this scope exists to remove. - protected reapplyViewportUserAgentOverride( - guest: Electron.WebContents, - browserTabId: string, - url: string - ): void { - const mobile = this.viewportUaOverrideMobileByTabId.get(browserTabId) - if (mobile === undefined) { - return - } - // Why: no queue needed — debugger.sendCommand dispatches in call order over one channel, so the - // later-issued write wins. What matters is that both writers resolve the SAME host, which they - // now do via the navigation target rather than the stale committed URL. - void this.sendViewportUserAgentOverride(guest, mobile, url).catch(() => {}) - } - - protected async sendViewportUserAgentOverride( - guest: Electron.WebContents, - mobile: boolean, - url?: string, - baseUserAgent?: string - ): Promise { - if (guest.isDestroyed() || !guest.debugger.isAttached()) { - return - } - await guest.debugger.sendCommand( - 'Emulation.setUserAgentOverride', - buildViewportUserAgentOverride({ - url: url ?? this.resolveTabNavigationUrl(guest), - mobile, - // Why: the session UA is the profile's stable base identity. guest.getUserAgent() is not: - // applyGoogleAuthUserAgent leaves it pinned to the Firefox auth UA once a guest switches to - // the CDP override, so reading it back here would republish that identity on ordinary hosts. - baseUserAgent: baseUserAgent ?? getBrowserProcessUserAgentIdentity().userAgent, - googleAuthEnabled: getBrowserProcessUserAgentIdentity().mode === 'clean' - }) - ) - } - /** Route guests own their own popup handler, so their denials arrive here instead. */ reportRouteGuestPopupBlocked(input: { openerWebContentsId: number; url: string }): void { this.forwardOrQueuePopupEvent(input.openerWebContentsId, { diff --git a/src/main/browser/browser-manager-registration.ts b/src/main/browser/browser-manager-registration.ts index 816c0d61cb2..f6f7b41bbb9 100644 --- a/src/main/browser/browser-manager-registration.ts +++ b/src/main/browser/browser-manager-registration.ts @@ -46,7 +46,7 @@ export abstract class BrowserManagerRegistration extends BrowserManagerGuestPoli const previousWebContentsId = this.webContentsIdByTabId.get(browserTabId) if (previousWebContentsId !== undefined && previousWebContentsId !== webContentsId) { this.retireStaleGuestWebContents(previousWebContentsId) - this.viewportPresetActiveByTabId.delete(browserTabId) + this.viewportPresetByTabId.delete(browserTabId) this.viewportScrollStateByTabId.delete(browserTabId) } this.webContentsIdByTabId.set(browserTabId, webContentsId) @@ -135,8 +135,7 @@ export abstract class BrowserManagerRegistration extends BrowserManagerGuestPoli this.worktreeIdByTabId.delete(browserTabId) // Why: drop the viewport-op chain so the Map doesn't retain a promise keyed to a destroyed guest. this.viewportOpsByTabId.delete(browserTabId) - this.viewportUaOverrideMobileByTabId.delete(browserTabId) - this.viewportPresetActiveByTabId.delete(browserTabId) + this.viewportPresetByTabId.delete(browserTabId) this.viewportScrollStateByTabId.delete(browserTabId) if (wcId !== undefined) { this.pendingNavigationByGuestId.delete(wcId) @@ -171,7 +170,7 @@ export abstract class BrowserManagerRegistration extends BrowserManagerGuestPoli const previousWebContentsId = this.webContentsIdByTabId.get(browserPageId) if (previousWebContentsId !== undefined && previousWebContentsId !== webContentsId) { this.retireStaleGuestWebContents(previousWebContentsId) - this.viewportPresetActiveByTabId.delete(browserPageId) + this.viewportPresetByTabId.delete(browserPageId) this.viewportScrollStateByTabId.delete(browserPageId) } this.webContentsIdByTabId.set(browserPageId, webContentsId) @@ -206,10 +205,9 @@ export abstract class BrowserManagerRegistration extends BrowserManagerGuestPoli this.pageInitiatedTabBudgetByRootGuestId.clear() this.worktreeIdByTabId.clear() this.sessionProfileIdByPageId.clear() - this.viewportUaOverrideMobileByTabId.clear() - this.viewportPresetActiveByTabId.clear() + this.viewportPresetByTabId.clear() this.viewportScrollStateByTabId.clear() - this.authUserAgentOverrideStateByGuestId.clear() + this.cdpUserAgentOverrideStateByGuestId.clear() this.pendingNavigationByGuestId.clear() this.pendingLoadFailuresByGuestId.clear() this.loadErrorsByGuestId.clear() diff --git a/src/main/browser/browser-manager-state.ts b/src/main/browser/browser-manager-state.ts index da7fa256b4e..da56552d6c4 100644 --- a/src/main/browser/browser-manager-state.ts +++ b/src/main/browser/browser-manager-state.ts @@ -9,7 +9,7 @@ import type { BrowserLoadError } from '../../shared/browser-workspace-types' import { resolveBrowserRouteGuestPopupOpener } from './browser-route-guest-popup-ownership' import type { ActiveDownload, - AuthUserAgentOverrideState, + CdpUserAgentOverrideState, PendingMainFrameNavigation, PendingPermissionEvent, PendingPopupEvent, @@ -125,14 +125,11 @@ export abstract class BrowserManagerState extends BrowserManagerViewportScrollSt protected readonly sessionProfileIdByPageId = new Map() // Why: serialize per-tab setViewportOverride so rapid toggles don't interleave CDP commands and leave emulation in a wrong state. protected readonly viewportOpsByTabId = new Map>() - // Why: presence means the preset requires a CDP UA override (installed or in flight), so navigation - // can re-issue it against the target URL's identity. - protected readonly viewportUaOverrideMobileByTabId = new Map() // Why: the confirmed CDP identity outranks getUserAgent; pending intent keeps rapid navigations // ordered without claiming a failed write was installed. - protected readonly authUserAgentOverrideStateByGuestId = new Map< + protected readonly cdpUserAgentOverrideStateByGuestId = new Map< number, - AuthUserAgentOverrideState + CdpUserAgentOverrideState >() // Why: the in-flight main-frame navigation target, held only until commit or failure — getURL() // still reports the outgoing page until then. See resolveTabNavigationUrl. @@ -191,12 +188,11 @@ export abstract class BrowserManagerState extends BrowserManagerViewportScrollSt this.settingsResolver = resolver } - // Why: a debugger detach clears every CDP override Chromium holds, including the Google auth-host - // UA override, so the confirmed-override record must be dropped or the next auth navigation - // believes the identity is still installed and skips the write. - protected trackDebuggerDetachForAuthUserAgent(guest: Electron.WebContents): () => void { + // Why: a debugger detach clears every CDP override Chromium holds, including the UA override, so + // the confirmed-override record must be dropped or navigation believes an override still stands. + protected trackDebuggerDetachForUserAgentOverride(guest: Electron.WebContents): () => void { const onDetach = (): void => { - this.authUserAgentOverrideStateByGuestId.delete(guest.id) + this.cdpUserAgentOverrideStateByGuestId.delete(guest.id) } try { guest.debugger.on('detach', onDetach) diff --git a/src/main/browser/browser-manager-tab-identity.test.ts b/src/main/browser/browser-manager-tab-identity.test.ts new file mode 100644 index 00000000000..d8678a6e2cd --- /dev/null +++ b/src/main/browser/browser-manager-tab-identity.test.ts @@ -0,0 +1,385 @@ +import { afterEach, beforeEach, 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 { googleAuthUserAgent } from './browser-google-auth-ua' +import { resetBrowserManagerMocks, resetBrowserManagerState } from './browser-manager-test-harness' +import { + createViewportGuestFactory, + flushViewportOps, + GUEST_CLEAN_UA, + GUEST_ELECTRON_UA, + type ViewportGuestHandle +} from './browser-manager-viewport-test-fixtures' + +const makeGuest = createViewportGuestFactory(mocks) +const guests = new Map>() +const PRESETS = { + none: null, + desktop: { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false }, + mobile: { width: 375, height: 667, deviceScaleFactor: 2, mobile: true } +} as const +type Preset = keyof typeof PRESETS +const MODES = [ + ['clean', GUEST_CLEAN_UA], + ['native', GUEST_ELECTRON_UA] +] as const +const ORDINARY_URL = 'https://example.com/' +const AUTH_URL = 'https://accounts.google.com/v3/signin/identifier' +const APP_TOKENS = /Electron\/|orca\//i +let nextGuestId = 7000 + +type Observed = { + presented: string + standingOverride: Record | null + requestIdentity: ReturnType +} + +/** Opens a tab on `url` the way a real load does: did-start-navigation runs before any preset. */ +function openTab(url: string): { tab: string; id: number; handle: ViewportGuestHandle } { + const id = nextGuestId++ + const tab = `tab-${id}` + const handle = makeGuest(id, url) + guests.set(id, handle.guest) + expect(browserManager.registerOffscreenGuest({ browserPageId: tab, webContentsId: id })).toBe( + true + ) + navigate(url) + return { tab, id, handle } +} + +function navigate(url: string, { inPlace = false } = {}): void { + const didStartNavigation = mocks.guestOnMock.mock.calls.findLast( + ([event]) => event === 'did-start-navigation' + )?.[1] + expect(didStartNavigation).toBeTypeOf('function') + didStartNavigation(null, url, inPlace, true) +} + +function redirectTo(url: string): void { + const willRedirect = mocks.guestOnMock.mock.calls.findLast( + ([event]) => event === 'will-redirect' + )?.[1] + willRedirect({ preventDefault: vi.fn() }, url, false, true) +} + +// A redirect target that fails: the one failed-load path whose restore could otherwise write the UA. +function failLoad(url: string): void { + redirectTo(url) + const didFailLoad = mocks.guestOnMock.mock.calls.findLast( + ([event]) => event === 'did-fail-load' + )?.[1] + didFailLoad(null, -102, 'ERR_CONNECTION_REFUSED', url, true) +} + +async function observe( + { id, handle }: { id: number; handle: ViewportGuestHandle }, + url: string +): Promise { + await flushViewportOps() + return { + presented: handle.presentedUserAgent(), + standingOverride: handle.standingUserAgentOverride(), + requestIdentity: browserManager.resolveBrowserGuestRequestUserAgent({ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads no Session member. + session: handle.guest.session as Electron.Session, + url, + webContentsId: id + }) + } +} + +async function presentWith(url: string, preset: Preset): Promise { + const opened = openTab(url) + const override = PRESETS[preset] + if (override) { + await browserManager.setViewportOverride(opened.tab, override) + } + return observe(opened, url) +} + +// Why: Chromium drops navigator.userAgentData and every sec-ch-ua header for a UA override that +// carries no userAgentMetadata, so only Firefox (which sends no hints) may stand without it. +function expectClientHintsKept(observed: Observed): void { + const override = observed.standingOverride + if (override && override.userAgent !== googleAuthUserAgent()) { + expect(override.userAgentMetadata).toBeDefined() + } +} + +describe('tab identity ownership', () => { + beforeEach(() => { + expect(process.env.ORCA_BACKGROUND_LAUNCH).toBe('1') + resetBrowserManagerMocks(mocks) + resetBrowserManagerState() + guests.clear() + mocks.webContentsFromIdMock.mockImplementation((id) => guests.get(id)) + }) + afterEach(() => { + browserManager.unregisterAll() + vi.restoreAllMocks() + }) + + describe.each(MODES)('in %s mode', (mode, processUserAgent) => { + beforeEach(() => { + mocks.processUserAgentMode = mode + mocks.processUserAgent = processUserAgent + }) + + it.each([ORDINARY_URL, AUTH_URL])( + 'presents the same identity with a desktop preset as with none on %s', + async (url) => { + const none = await presentWith(url, 'none') + const desktop = await presentWith(url, 'desktop') + expect(desktop).toEqual(none) + expectClientHintsKept(desktop) + const expected = + mode === 'clean' && url === AUTH_URL ? googleAuthUserAgent() : processUserAgent + expect(desktop.presented).toBe(expected) + expect(desktop.requestIdentity.userAgent).toBe(expected) + expect(desktop.requestIdentity.kind).toBe( + expected === processUserAgent ? 'process' : 'google-auth' + ) + } + ) + + it('presents the mobile identity with matching client hints on an ordinary host', async () => { + const observed = await presentWith(ORDINARY_URL, 'mobile') + expect(observed.presented).toContain('iPhone') + expect(observed.standingOverride).toMatchObject({ + userAgentMetadata: expect.objectContaining({ mobile: true, platform: 'iOS' }) + }) + expect(observed.requestIdentity.kind).toBe('mobile') + expect(observed.requestIdentity.userAgent).toBe(observed.presented) + }) + + it('keeps the Google sign-in identity ahead of a mobile preset only in clean mode', async () => { + const observed = await presentWith(AUTH_URL, 'mobile') + expectClientHintsKept(observed) + if (mode === 'clean') { + expect(observed.presented).toBe(googleAuthUserAgent()) + expect(observed.requestIdentity.kind).toBe('google-auth') + } else { + expect(observed.presented).toContain('iPhone') + expect(observed.requestIdentity.kind).toBe('mobile') + } + }) + + it.each(['desktop', 'none'] as const)( + 'restores the process identity when switching from mobile to %s', + async (next) => { + const opened = openTab(ORDINARY_URL) + await browserManager.setViewportOverride(opened.tab, PRESETS.mobile) + expect(opened.handle.presentedUserAgent()).toContain('iPhone') + + await browserManager.setViewportOverride(opened.tab, PRESETS[next]) + const observed = await observe(opened, ORDINARY_URL) + expect(observed.presented).toBe(processUserAgent) + expect(observed.standingOverride).toBeNull() + expect(observed.requestIdentity).toEqual({ kind: 'process', userAgent: processUserAgent }) + } + ) + }) + + it('never puts the Electron or app tokens on a clean-mode tab under any preset', async () => { + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + for (const url of [ORDINARY_URL, AUTH_URL]) { + for (const preset of ['none', 'desktop', 'mobile'] as const) { + const observed = await presentWith(url, preset) + expect(observed.presented).not.toMatch(APP_TOKENS) + expect(observed.requestIdentity.userAgent).not.toMatch(APP_TOKENS) + } + } + }) + + // Why: Chromium rejects touch emulation with maxTouchPoints 0, so every step after it used to be + // skipped and the mobile UA outlived its preset. Identity is no longer a step after touch. + it('restores the process identity and reports the failure when touch emulation is rejected', async () => { + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const opened = openTab(ORDINARY_URL) + await browserManager.setViewportOverride(opened.tab, PRESETS.mobile) + // Rejects whatever the payload, so the test outlives a fix to the payload itself. + opened.handle.debuggerSendCommand.mockImplementation((method: string) => + method === 'Emulation.setTouchEmulationEnabled' + ? Promise.reject(new Error('Touch points must be between 1 and 16')) + : Promise.resolve(undefined) + ) + + await expect(browserManager.setViewportOverride(opened.tab, PRESETS.desktop)).resolves.toBe( + false + ) + const observed = await observe(opened, ORDINARY_URL) + expect(observed.presented).toBe(GUEST_CLEAN_UA) + expect(observed.standingOverride).toBeNull() + expect(warn).toHaveBeenCalledWith( + '[browser-manager] setViewportOverride: touch emulation failed', + expect.objectContaining({ error: 'Touch points must be between 1 and 16' }) + ) + }) + + it('still presents the requested mobile identity when device metrics fail', async () => { + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + vi.spyOn(console, 'warn').mockImplementation(() => {}) + const opened = openTab(ORDINARY_URL) + opened.handle.debuggerSendCommand.mockImplementation((method: string) => + method === 'Emulation.setDeviceMetricsOverride' + ? Promise.reject(new Error('metrics failed')) + : Promise.resolve(undefined) + ) + + await expect(browserManager.setViewportOverride(opened.tab, PRESETS.mobile)).resolves.toBe( + false + ) + expect((await observe(opened, ORDINARY_URL)).presented).toContain('iPhone') + }) + + it('heals a mid-redirect process override on the next navigation', async () => { + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + const opened = openTab(AUTH_URL) + await browserManager.setViewportOverride(opened.tab, PRESETS.desktop) + const willRedirect = mocks.guestOnMock.mock.calls.findLast( + ([event]) => event === 'will-redirect' + )?.[1] + willRedirect({ preventDefault: vi.fn() }, ORDINARY_URL, false, true) + // The WebContents UA still says Firefox, so clearing here would expose it on an ordinary host. + expect((await observe(opened, ORDINARY_URL)).presented).toBe(GUEST_CLEAN_UA) + + navigate('https://example.org/') + const healed = await observe(opened, 'https://example.org/') + expect(healed.presented).toBe(GUEST_CLEAN_UA) + expect(healed.standingOverride).toBeNull() + }) + + // Why a table over every entry point: Chromium cancels a redirect, and reloads a loading document, + // when the WebContents UA changes anywhere but a cross-document navigation start. + it.each([ + ['a redirect', () => redirectTo(ORDINARY_URL)], + ['a failed load', () => failLoad(ORDINARY_URL)], + ['a desktop preset', (tab: string) => browserManager.setViewportOverride(tab, PRESETS.desktop)], + ['a mobile preset', (tab: string) => browserManager.setViewportOverride(tab, PRESETS.mobile)], + ['clearing a preset', (tab: string) => browserManager.setViewportOverride(tab, null)], + ['a same-document navigation', () => navigate(`${AUTH_URL}#step`, { inPlace: true })] + ])( + 'writes the WebContents UA only at a cross-document navigation start, not on %s', + async (_label, act) => { + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + const opened = openTab(AUTH_URL) + expect(opened.handle.guest.setUserAgent).toHaveBeenCalledOnce() + + await act(opened.tab) + await flushViewportOps() + expect(opened.handle.guest.setUserAgent).toHaveBeenCalledOnce() + + navigate('https://example.org/') + // Synchronous: the write must land inside did-start-navigation, before any await. + expect(opened.handle.guest.setUserAgent).toHaveBeenCalledTimes(2) + expect(opened.handle.webContentsUserAgent()).toBe(GUEST_CLEAN_UA) + } + ) + + // Why: setUserAgent() while a document loads makes Chromium reload it, and an OAuth callback that + // strips its code with replaceState would be requested twice — the one-time code replayed. + it('leaves the WebContents UA alone on a same-document navigation', async () => { + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + const opened = openTab(AUTH_URL) + const willRedirect = mocks.guestOnMock.mock.calls.findLast( + ([event]) => event === 'will-redirect' + )?.[1] + willRedirect({ preventDefault: vi.fn() }, `${ORDINARY_URL}callback?code=1`, false, true) + await flushViewportOps() + + navigate(`${ORDINARY_URL}callback`, { inPlace: true }) + expect(opened.handle.webContentsUserAgent()).toBe(googleAuthUserAgent()) + expect((await observe(opened, ORDINARY_URL)).presented).toBe(GUEST_CLEAN_UA) + + navigate('https://example.org/') + expect(opened.handle.webContentsUserAgent()).toBe(GUEST_CLEAN_UA) + }) + + // Why: a failed navigation commits an error page that is still loading at did-fail-load, and + // setUserAgent() then makes Chromium reload it — the failed request replayed. + it('leaves the WebContents UA alone when a redirected navigation fails', async () => { + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + const opened = openTab(ORDINARY_URL) + navigate(`${ORDINARY_URL}start`) + const willRedirect = mocks.guestOnMock.mock.calls.findLast( + ([event]) => event === 'will-redirect' + )?.[1] + willRedirect({ preventDefault: vi.fn() }, AUTH_URL, false, true) + await flushViewportOps() + + opened.handle.commitNavigationTo(AUTH_URL) + const didFailLoad = mocks.guestOnMock.mock.calls.findLast( + ([event]) => event === 'did-fail-load' + )?.[1] + didFailLoad(null, -102, 'ERR_CONNECTION_REFUSED', AUTH_URL, true) + expect(opened.handle.webContentsUserAgent()).toBe(GUEST_CLEAN_UA) + const observed = await observe(opened, AUTH_URL) + expect(observed.presented).toBe(googleAuthUserAgent()) + expect(observed.requestIdentity.kind).toBe('google-auth') + }) + + // Why: a debugger that cannot attach (DevTools open on the guest) installs no mobile identity, so + // requests claiming one would disagree with the document's own navigator.userAgent. + it('keeps requests on the presented identity when a mobile preset cannot attach', async () => { + mocks.processUserAgentMode = 'clean' + mocks.processUserAgent = GUEST_CLEAN_UA + vi.spyOn(console, 'warn').mockImplementation(() => {}) + const opened = openTab(ORDINARY_URL) + opened.handle.debuggerIsAttached.mockReturnValue(false) + opened.handle.debuggerAttach.mockImplementation(() => { + throw new Error('Another debugger is already attached') + }) + + await expect(browserManager.setViewportOverride(opened.tab, PRESETS.mobile)).resolves.toBe( + false + ) + navigate('https://example.org/') + const observed = await observe(opened, 'https://example.org/') + expect(observed.presented).toBe(GUEST_CLEAN_UA) + expect(observed.requestIdentity).toEqual({ kind: 'process', userAgent: GUEST_CLEAN_UA }) + }) +}) diff --git a/src/main/browser/browser-manager-types.ts b/src/main/browser/browser-manager-types.ts index 3d3825d6ebe..302cf6468ed 100644 --- a/src/main/browser/browser-manager-types.ts +++ b/src/main/browser/browser-manager-types.ts @@ -1,4 +1,5 @@ import { normalizeExternalBrowserUrl } from '../../shared/browser-url' +import type { UserAgentMetadata } from './browser-tab-identity' import type { BrowserDownloadFinishedEvent, BrowserDownloadProgressEvent, @@ -60,15 +61,21 @@ export type PendingMainFrameNavigation = { supersededUrls: string[] } -export type AuthUserAgentOverrideOperation = { - sequence: number +/** An Emulation.setUserAgentOverride payload; an empty userAgent clears the override. */ +export type CdpUserAgentOverride = { userAgent: string + userAgentMetadata?: UserAgentMetadata } -export type AuthUserAgentOverrideState = { - confirmed: AuthUserAgentOverrideOperation | null +export type CdpUserAgentOverrideOperation = { + sequence: number + override: CdpUserAgentOverride +} + +export type CdpUserAgentOverrideState = { + confirmed: CdpUserAgentOverrideOperation | null nextSequence: number - pending: AuthUserAgentOverrideOperation[] + pending: CdpUserAgentOverrideOperation[] } export const SAFE_POPUP_WINDOW_OPTIONS = { diff --git a/src/main/browser/browser-manager-viewport-override.test.ts b/src/main/browser/browser-manager-viewport-override.test.ts index 41835a6cd3e..a8a4f3bacd4 100644 --- a/src/main/browser/browser-manager-viewport-override.test.ts +++ b/src/main/browser/browser-manager-viewport-override.test.ts @@ -156,7 +156,8 @@ describe('browserManager', () => { if (mobile) { expect(userAgentOverride).toMatchObject({ userAgent: expect.stringContaining('iPhone') }) } else { - expect(userAgentOverride).toEqual({ userAgent: GUEST_ELECTRON_UA }) + // A desktop preset presents the process identity itself, so there is nothing to override. + expect(userAgentOverride).toBeUndefined() } } ) @@ -193,8 +194,8 @@ describe('browserManager', () => { } ) - it('re-issues the standing UA override when navigating onto and back off an auth host', async () => { - const { guest, debuggerSendCommand } = makeGuest(4247) + it('keeps a desktop-preset tab on the right identity onto and back off an auth host', async () => { + const { guest, debuggerSendCommand, presentedUserAgent } = makeGuest(4247) webContentsFromIdMock.mockReturnValue(guest) browserManager.attachGuestPolicies(guest as never) browserManager.registerGuest({ @@ -214,30 +215,25 @@ describe('browserManager', () => { isMainFrame: boolean ) => void - // Case A: the desktop preset lands first, while the tab is still off the auth host. + // The desktop preset lands first, while the tab is still off the auth host. await browserManager.setViewportOverride('tab-auth-nav', { width: 1024, height: 768, deviceScaleFactor: 1, mobile: false }) - expect(debuggerSendCommand).toHaveBeenLastCalledWith('Emulation.setUserAgentOverride', { - userAgent: GUEST_CLEAN_UA - }) + expect(lastUserAgentOverride(debuggerSendCommand)).toBeUndefined() + expect(presentedUserAgent()).toBe(GUEST_CLEAN_UA) - // Navigating to the auth host must move the standing override to the Firefox identity. - debuggerSendCommand.mockClear() didStartNavigation(null, 'https://accounts.google.com/v3/signin/identifier', false, true) await flushViewportOps() - expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: googleAuthUserAgent() - }) + expect(presentedUserAgent()).toBe(googleAuthUserAgent()) - // Leaving the auth host restores the clean Chrome-shaped preset UA. - debuggerSendCommand.mockClear() + // A redirect cannot rewrite the Firefox WebContents UA, so the override carries the process UA. willRedirect({ preventDefault: vi.fn() }, 'https://example.com/', false, true) await flushViewportOps() - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ userAgent: GUEST_CLEAN_UA }) + expect(guest.setUserAgent).toHaveBeenCalledTimes(1) + expect(presentedUserAgent()).toBe(GUEST_CLEAN_UA) }) // Why: not an ordering race — debugger.sendCommand dispatches in call order over one channel, so @@ -255,12 +251,13 @@ describe('browserManager', () => { return calls.at(-1)?.[1] as Record | undefined } - // Why mobile: on the desktop branch the break is masked by coincidence — applyGoogleAuthUserAgent - // has already switched the WebContents UA to Firefox, and cleanElectronUserAgent passes a Firefox - // UA through untouched, so the stale-URL desktop path happens to emit Firefox anyway. The mobile - // branch derives a Chrome-shaped iPhone UA from that same base and exposes the real defect. + // Why mobile: a desktop preset installs no override at all, so only the mobile write can leave a + // Chrome-shaped UA standing on the auth host. it('does not leave the Chrome preset UA standing when a mobile preset lands mid-navigation onto an auth host', async () => { - const { guest, debuggerSendCommand } = makeGuest(4251, 'https://example.com/') + const { guest, debuggerSendCommand, presentedUserAgent } = makeGuest( + 4251, + 'https://example.com/' + ) // Hold the preset's first CDP command open so the navigation lands inside its await window. let releaseMetrics = (): void => {} const metricsGate = new Promise((resolve) => { @@ -294,13 +291,18 @@ describe('browserManager', () => { // Without a shared chain + shared URL source, the preset resumes, reads getURL() as the // pre-navigation host, and pins the tab to the Chrome-shaped UA while it is on the auth host. - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ - userAgent: googleAuthUserAgent() - }) + expect(presentedUserAgent()).toBe(googleAuthUserAgent()) + expect(debuggerSendCommand).not.toHaveBeenCalledWith( + 'Emulation.setUserAgentOverride', + expect.objectContaining({ userAgent: expect.stringContaining('iPhone') }) + ) }) it('does not leave the Firefox UA standing when a preset lands mid-navigation off an auth host', async () => { - const { guest, debuggerSendCommand } = makeGuest(4252, 'https://accounts.google.com/') + const { guest, debuggerSendCommand, presentedUserAgent } = makeGuest( + 4252, + 'https://accounts.google.com/' + ) webContentsFromIdMock.mockReturnValue(guest) browserManager.attachGuestPolicies(guest as never) browserManager.registerGuest({ @@ -347,11 +349,12 @@ describe('browserManager', () => { // Without the fix the resuming preset re-reads getURL() as the auth host and clobbers the // navigation's correct write, stranding the Firefox UA on a non-auth page. - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ userAgent: GUEST_CLEAN_UA }) + expect(presentedUserAgent()).toBe(GUEST_CLEAN_UA) + expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ userAgent: '' }) }) it('falls back to the committed URL once a navigation commits or fails', async () => { - const { guest, debuggerSendCommand, commitNavigationTo } = makeGuest( + const { guest, debuggerSendCommand, commitNavigationTo, presentedUserAgent } = makeGuest( 4253, 'https://example.com/' ) @@ -392,8 +395,9 @@ describe('browserManager', () => { didFailLoad(null, -3, 'Aborted', 'https://accounts.google.com/', true) await flushViewportOps() - expect(guest.setUserAgent).toHaveBeenLastCalledWith(GUEST_CLEAN_UA) - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ userAgent: GUEST_CLEAN_UA }) + // The restore goes over CDP: only did-start-navigation wrote the WebContents UA. + expect(guest.setUserAgent).toHaveBeenCalledOnce() + expect(presentedUserAgent()).toBe(GUEST_CLEAN_UA) // A later preset must also resolve the committed, non-auth URL. debuggerSendCommand.mockClear() @@ -407,7 +411,6 @@ describe('browserManager', () => { didNavigate(null, 'https://accounts.google.com/signin') await flushViewportOps() - debuggerSendCommand.mockClear() await browserManager.setViewportOverride('tab-pending-cleared', { width: 1024, height: 768, @@ -415,13 +418,14 @@ describe('browserManager', () => { mobile: false }) await flushViewportOps() - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ - userAgent: googleAuthUserAgent() - }) + expect(presentedUserAgent()).toBe(googleAuthUserAgent()) }) it('does not let a superseded navigation failure revert a newer target', async () => { - const { guest, debuggerSendCommand } = makeGuest(4255, 'https://example.com/') + const { guest, debuggerSendCommand, presentedUserAgent } = makeGuest( + 4255, + 'https://example.com/' + ) webContentsFromIdMock.mockReturnValue(guest) browserManager.attachGuestPolicies(guest as never) browserManager.registerGuest({ @@ -460,20 +464,19 @@ describe('browserManager', () => { willRedirect({ preventDefault: vi.fn() }, 'https://accounts.google.com/same', false, true) didStartNavigation(null, 'https://accounts.google.com/same', false, true) await flushViewportOps() - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ - userAgent: googleAuthUserAgent() - }) + expect(presentedUserAgent()).toBe(googleAuthUserAgent()) debuggerSendCommand.mockClear() didFailLoad(null, -3, 'Aborted', 'https://accounts.google.com/same', true) await flushViewportOps() - // Why: the redirect path never writes the WebContents UA — that write cancels the navigation. - expect(guest.setUserAgent).not.toHaveBeenCalled() + // Only the direct navigation onto the auth host wrote the WebContents UA. + expect(guest.setUserAgent).toHaveBeenCalledOnce() expect(debuggerSendCommand).not.toHaveBeenCalledWith( 'Emulation.setUserAgentOverride', - expect.objectContaining({ userAgent: GUEST_CLEAN_UA }) + expect.anything() ) + expect(presentedUserAgent()).toBe(googleAuthUserAgent()) }) it('switches identity for a server redirect and restores it if the redirect fails', async () => { @@ -529,10 +532,11 @@ describe('browserManager', () => { userAgent: googleAuthUserAgent() }) + // The WebContents UA never left the process identity, so restoring it clears the override. didFailLoad(null, -3, 'Aborted', 'https://accounts.google.com/redirected', true) await flushViewportOps() expect(guest.setUserAgent).not.toHaveBeenCalled() - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ userAgent: GUEST_CLEAN_UA }) + expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ userAgent: '' }) }) it('preserves the auth identity when a viewport preset is cleared after a redirect', async () => { @@ -584,7 +588,11 @@ describe('browserManager', () => { }) await browserManager.setViewportOverride('tab-mobile-popup-owner', MOBILE_VIEWPORT_OVERRIDE) - const { guest: popupGuest, debuggerSendCommand } = makeGuest(4264, 'https://mail.google.com/') + const { + guest: popupGuest, + debuggerSendCommand, + presentedUserAgent + } = makeGuest(4264, 'https://mail.google.com/') const popupOn = vi.fn() popupGuest.on = popupOn browserManager.attachGuestPolicies(popupGuest as never, { @@ -607,11 +615,18 @@ describe('browserManager', () => { didStartNavigation(null, 'https://example.com/', false, true) await flushViewportOps() - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ userAgent: GUEST_CLEAN_UA }) + expect(presentedUserAgent()).toBe(GUEST_CLEAN_UA) + expect(debuggerSendCommand).not.toHaveBeenCalledWith( + 'Emulation.setUserAgentOverride', + expect.objectContaining({ userAgent: expect.stringContaining('iPhone') }) + ) }) it('reapplies a preset when navigation starts during its final UA write', async () => { - const { guest, debuggerSendCommand } = makeGuest(4257, 'https://example.com/') + const { guest, debuggerSendCommand, presentedUserAgent } = makeGuest( + 4257, + 'https://example.com/' + ) let releaseFirstUa = (): void => {} const firstUaGate = new Promise((resolve) => { releaseFirstUa = resolve @@ -634,25 +649,26 @@ describe('browserManager', () => { ([event]) => event === 'did-start-navigation' )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void - const presetDone = browserManager.setViewportOverride('tab-final-apply-race', { - width: 1024, - height: 768, - deviceScaleFactor: 1, - mobile: false - }) + const presetDone = browserManager.setViewportOverride( + 'tab-final-apply-race', + MOBILE_VIEWPORT_OVERRIDE + ) await flushViewportOps() didStartNavigation(null, 'https://accounts.google.com/', false, true) await flushViewportOps() - expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ - userAgent: googleAuthUserAgent() - }) + // The navigation's write is issued after the held mobile write, so it wins however they settle. releaseFirstUa() await presetDone + await flushViewportOps() + expect(presentedUserAgent()).toBe(googleAuthUserAgent()) }) it('does not reinstall a preset while its final UA clear is in flight', async () => { - const { guest, debuggerSendCommand } = makeGuest(4258, 'https://example.com/') + const { guest, debuggerSendCommand, presentedUserAgent } = makeGuest( + 4258, + 'https://example.com/' + ) webContentsFromIdMock.mockReturnValue(guest) browserManager.attachGuestPolicies(guest as never) browserManager.registerGuest({ @@ -664,12 +680,7 @@ describe('browserManager', () => { ([event]) => event === 'did-start-navigation' )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void - await browserManager.setViewportOverride('tab-final-clear-race', { - width: 1024, - height: 768, - deviceScaleFactor: 1, - mobile: false - }) + await browserManager.setViewportOverride('tab-final-clear-race', MOBILE_VIEWPORT_OVERRIDE) let releaseClearUa = (): void => {} const clearUaGate = new Promise((resolve) => { releaseClearUa = resolve @@ -686,19 +697,25 @@ describe('browserManager', () => { debuggerSendCommand.mockClear() didStartNavigation(null, 'https://accounts.google.com/', false, true) await flushViewportOps() - expect(debuggerSendCommand).not.toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: googleAuthUserAgent() - }) + expect(debuggerSendCommand).not.toHaveBeenCalledWith( + 'Emulation.setUserAgentOverride', + expect.objectContaining({ userAgent: expect.stringContaining('iPhone') }) + ) releaseClearUa() await clearDone + await flushViewportOps() + expect(presentedUserAgent()).toBe(googleAuthUserAgent()) }) // Why: a failed clear leaves the CDP override standing on the target. Dropping the tracking entry // first makes it untracked, so the navigation path can never correct it again and the tab carries // a Chrome-shaped navigator.userAgent onto the auth hosts — the exact failure this PR prevents. it('keeps tracking the standing override when the CDP clear fails', async () => { - const { guest, debuggerSendCommand } = makeGuest(4254, 'https://example.com/') + const { guest, debuggerSendCommand, presentedUserAgent } = makeGuest( + 4254, + 'https://example.com/' + ) webContentsFromIdMock.mockReturnValue(guest) browserManager.attachGuestPolicies(guest as never) browserManager.registerGuest({ @@ -710,15 +727,10 @@ describe('browserManager', () => { ([event]) => event === 'did-start-navigation' )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void - await browserManager.setViewportOverride('tab-failed-clear', { - width: 1024, - height: 768, - deviceScaleFactor: 1, - mobile: false - }) + await browserManager.setViewportOverride('tab-failed-clear', MOBILE_VIEWPORT_OVERRIDE) await flushViewportOps() - // The final UA clear fails after tracking was optimistically removed. + // The final UA clear fails, leaving the mobile override standing. debuggerSendCommand.mockImplementation( (method: string, params: { userAgent?: string } | undefined) => method === 'Emulation.setUserAgentOverride' && params?.userAgent === '' @@ -736,8 +748,9 @@ describe('browserManager', () => { didStartNavigation(null, 'https://accounts.google.com/v3/signin/identifier', false, true) await flushViewportOps() expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: googleAuthUserAgent() + userAgent: '' }) + expect(presentedUserAgent()).toBe(googleAuthUserAgent()) }) it('does not touch the UA override on navigation when no preset is standing', async () => { @@ -774,12 +787,7 @@ describe('browserManager', () => { ([event]) => event === 'did-start-navigation' )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void - await browserManager.setViewportOverride('tab-cleared-preset', { - width: 1024, - height: 768, - deviceScaleFactor: 1, - mobile: false - }) + await browserManager.setViewportOverride('tab-cleared-preset', MOBILE_VIEWPORT_OVERRIDE) await browserManager.setViewportOverride('tab-cleared-preset', null) debuggerSendCommand.mockClear() @@ -791,7 +799,7 @@ describe('browserManager', () => { ) }) - it('reapplies the native process identity instead of the Google exception', async () => { + it('keeps a native-mode mobile preset off the Google exception on navigation', async () => { browserMocks.processUserAgentMode = 'native' browserMocks.processUserAgent = GUEST_ELECTRON_UA const { guest, debuggerSendCommand } = makeGuest(4250) @@ -807,17 +815,13 @@ describe('browserManager', () => { ([event]) => event === 'did-start-navigation' )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void - await browserManager.setViewportOverride('tab-native-nav', { - width: 1024, - height: 768, - deviceScaleFactor: 1, - mobile: false - }) + await browserManager.setViewportOverride('tab-native-nav', MOBILE_VIEWPORT_OVERRIDE) debuggerSendCommand.mockClear() didStartNavigation(null, 'https://accounts.google.com/', false, true) await flushViewportOps() - expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: GUEST_ELECTRON_UA + expect(guest.setUserAgent).not.toHaveBeenCalled() + expect(lastUserAgentOverride(debuggerSendCommand)).toMatchObject({ + userAgent: expect.stringContaining('iPhone') }) }) @@ -839,9 +843,26 @@ describe('browserManager', () => { enabled: false, maxTouchPoints: 0 }) - expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: '' + // Nothing overrode the identity, so there is nothing to clear. + expect(lastUserAgentOverride(debuggerSendCommand)).toBeUndefined() + }) + + it('clears the mobile identity for override=null', async () => { + const { guest, debuggerSendCommand, presentedUserAgent } = makeGuest(4344) + webContentsFromIdMock.mockReturnValue(guest) + browserManager.attachGuestPolicies(guest as never) + browserManager.registerGuest({ + browserPageId: 'tab-clear-mobile', + webContentsId: guest.id as number, + rendererWebContentsId }) + + await browserManager.setViewportOverride('tab-clear-mobile', MOBILE_VIEWPORT_OVERRIDE) + await expect(browserManager.setViewportOverride('tab-clear-mobile', null)).resolves.toBe(true) + await flushViewportOps() + + expect(lastUserAgentOverride(debuggerSendCommand)).toEqual({ userAgent: '' }) + expect(presentedUserAgent()).toBe(GUEST_CLEAN_UA) }) it('attaches the debugger if not already attached and does not detach after', async () => { diff --git a/src/main/browser/browser-manager-viewport-ownership.test.ts b/src/main/browser/browser-manager-viewport-ownership.test.ts index 8b4492f36fd..db2d0b9a38f 100644 --- a/src/main/browser/browser-manager-viewport-ownership.test.ts +++ b/src/main/browser/browser-manager-viewport-ownership.test.ts @@ -50,16 +50,16 @@ 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 presetIntents = readViewportStateMap('viewportPresetByTabId') const pendingOperations = readViewportStateMap('viewportOpsByTabId') +const cdpUserAgentOverrides = readViewportStateMap('cdpUserAgentOverrideStateByGuestId') function readViewportStateMap( name: | 'webContentsIdByTabId' - | 'viewportUaOverrideMobileByTabId' - | 'viewportPresetActiveByTabId' + | 'viewportPresetByTabId' | 'viewportOpsByTabId' + | 'cdpUserAgentOverrideStateByGuestId' ): Map { const value: unknown = browserManager[name] if (!(value instanceof Map)) { @@ -116,7 +116,7 @@ describe('browser viewport operation ownership', () => { const result = browserManager.setViewportOverride('closed', mobile) await gate.entered browserManager.unregisterGuest('closed') - expect(uaIntents.size).toBe(0) + expect(presetIntents.size).toBe(0) const isDestroyed = handle.guest.isDestroyed expect(vi.isMockFunction(isDestroyed)).toBe(true) if (vi.isMockFunction(isDestroyed)) { @@ -126,7 +126,7 @@ describe('browser viewport operation ownership', () => { await expect(result).resolves.toBe(false) expect(registeredGuests.size).toBe(0) expect(presetIntents.size).toBe(0) - expect(uaIntents.get('closed')).toBeUndefined() + expect(cdpUserAgentOverrides.size).toBe(0) }) it('does not restore closed-tab UA intent after a failed clear', async () => { @@ -143,7 +143,8 @@ describe('browser viewport operation ownership', () => { } gate.reject(new Error('Target closed')) await expect(result).resolves.toBe(false) - expect(uaIntents.get('clear-close')).toBeUndefined() + expect(presetIntents.get('clear-close')).toBeUndefined() + expect(cdpUserAgentOverrides.size).toBe(0) }) it('preserves replacement desktop intent after an old clear fails', async () => { @@ -155,14 +156,12 @@ describe('browser viewport operation ownership', () => { 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 + override: desktop }) }) @@ -177,11 +176,13 @@ describe('browser viewport operation ownership', () => { await browserManager.setViewportOverride('late-delete', mobile) gate.resolve() await expect(result).resolves.toBe(false) - expect(uaIntents.has('late-delete')).toBe(true) + expect(presetIntents.get('late-delete')).toEqual({ guestWebContentsId: 105, override: mobile }) expect(registeredGuests.get('late-delete')).toBe(105) }) - it('same-owner clear failure still restores the legitimate earlier intent', async () => { + // The request is still "no preset"; what survives the failure is the record that the mobile + // override stands, which is what lets the next navigation clear it. + it('same-owner clear failure keeps the standing mobile override tracked', async () => { const handle = register('same-owner', 106) await browserManager.setViewportOverride('same-owner', mobile) const gate = pause(handle, 'Emulation.setUserAgentOverride') @@ -189,7 +190,9 @@ describe('browser viewport operation ownership', () => { await gate.entered gate.reject(new Error('Protocol error')) await expect(result).resolves.toBe(false) - expect(uaIntents.get('same-owner')).toBe(true) + expect(presetIntents.get('same-owner')).toEqual({ guestWebContentsId: 106, override: null }) + expect(handle.presentedUserAgent()).toContain('iPhone') + expect(cdpUserAgentOverrides.has(106)).toBe(true) }) it('old apply cannot overwrite a replacement guest desktop intent', async () => { @@ -202,7 +205,11 @@ describe('browser viewport operation ownership', () => { await browserManager.setViewportOverride('late-apply', desktop) gate.resolve() await expect(pending).resolves.toBe(false) - expect(uaIntents.get('late-apply')).toBe(false) + expect(presetIntents.get('late-apply')).toEqual({ guestWebContentsId: 111, override: desktop }) + expect(old.debuggerSendCommand).not.toHaveBeenCalledWith( + 'Emulation.setUserAgentOverride', + expect.anything() + ) }) it('an old guest cannot write UA intent after replacement in native process mode', async () => { @@ -216,7 +223,11 @@ describe('browser viewport operation ownership', () => { register('native-replacement', 113) gate.resolve() await expect(pending).resolves.toBe(false) - expect(uaIntents.get('native-replacement')).toBeUndefined() + expect(presetIntents.get('native-replacement')).toBeUndefined() + expect(old.debuggerSendCommand).not.toHaveBeenCalledWith( + 'Emulation.setUserAgentOverride', + expect.anything() + ) }) it('old queued operations cannot remove or join a replacement promise tail', async () => { @@ -240,7 +251,11 @@ describe('browser viewport operation ownership', () => { await expect(replacementFirst).resolves.toBe(true) await expect(replacementSecond).resolves.toBe(true) expect(pendingOperations.size).toBe(0) - expect(uaIntents.get('queued-replacement')).toBe(true) + expect(presetIntents.get('queued-replacement')).toEqual({ + guestWebContentsId: 115, + override: mobile + }) + expect(replacement.presentedUserAgent()).toContain('iPhone') }) it('normal same-owner toggles preserve last-requested order and remove the promise tail', async () => { @@ -252,19 +267,18 @@ describe('browser viewport operation ownership', () => { const third = browserManager.setViewportOverride('serialized', null) gate.resolve() expect(await Promise.all([first, second, third])).toEqual([true, true, true]) + // Identity follows the last request, which was "no preset" before any identity write ran, so the + // mobile UA is never installed at all. 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' + 'Emulation.setTouchEmulationEnabled' ]) expect(pendingOperations.size).toBe(0) - expect(uaIntents.size).toBe(0) + expect(presetIntents.get('serialized')).toEqual({ guestWebContentsId: 116, override: null }) }) it.each([false, true])( @@ -276,13 +290,20 @@ describe('browser viewport operation ownership', () => { 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 } + if (mobileMode) { + expect(handle.debuggerSendCommand).toHaveBeenCalledWith( + 'Emulation.setUserAgentOverride', + expect.objectContaining({ userAgent: expect.stringContaining('iPhone') }) + ) + } else { + expect(handle.debuggerSendCommand).not.toHaveBeenCalledWith( + 'Emulation.setUserAgentOverride', + expect.anything() + ) + } + expect(handle.presentedUserAgent()).toEqual( + mobileMode ? expect.stringContaining('iPhone') : GUEST_ELECTRON_UA ) - expect(uaIntents.get('native')).toBe(mobileMode) } ) @@ -304,7 +325,7 @@ describe('browser viewport operation ownership', () => { expect(await Promise.all(operations.map(({ pending }) => pending))).toEqual( Array(16).fill(false) ) - expect(uaIntents.size).toBe(0) + expect(cdpUserAgentOverrides.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-scroll-state.ts b/src/main/browser/browser-manager-viewport-scroll-state.ts index 1f83534c856..04063fb3c30 100644 --- a/src/main/browser/browser-manager-viewport-scroll-state.ts +++ b/src/main/browser/browser-manager-viewport-scroll-state.ts @@ -1,5 +1,8 @@ import { webContents } from 'electron' -import type { BrowserViewportScrollState } from '../../shared/browser-workspace-types' +import type { + BrowserViewportOverride, + BrowserViewportScrollState +} from '../../shared/browser-workspace-types' /** * Renderer routing plus the host-side viewport-preset geometry the wheel path needs to decide @@ -7,11 +10,11 @@ import type { BrowserViewportScrollState } from '../../shared/browser-workspace- */ export abstract class BrowserManagerViewportScrollState { protected readonly rendererWebContentsIdByTabId = new Map() - // Why: host-side wheel panning follows the requested local viewport on the owning guest; - // replacement guests must not inherit a retired guest's state. - protected readonly viewportPresetActiveByTabId = new Map< + // Why: the requested preset, not the applied one — host-side wheel panning and the tab's identity + // both follow what was asked for; replacement guests must not inherit a retired guest's preset. + protected readonly viewportPresetByTabId = new Map< string, - { guestWebContentsId: number; active: boolean } + { guestWebContentsId: number; override: BrowserViewportOverride | null } >() protected readonly viewportScrollStateByTabId = new Map() diff --git a/src/main/browser/browser-manager-viewport-test-fixtures.ts b/src/main/browser/browser-manager-viewport-test-fixtures.ts index b440a768b71..4d6437cf5d9 100644 --- a/src/main/browser/browser-manager-viewport-test-fixtures.ts +++ b/src/main/browser/browser-manager-viewport-test-fixtures.ts @@ -21,19 +21,47 @@ export type ViewportGuestHandle = { debuggerAttach: ReturnType setGuestUserAgent: (ua: string) => void commitNavigationTo: (nextUrl: string) => void + webContentsUserAgent: () => string + /** What navigator.userAgent reports: the latest applied CDP override, else the WebContents UA. */ + presentedUserAgent: () => string + /** The CDP UA override Chromium holds, or null when none stands. */ + standingUserAgentOverride: () => Record | null } // Why: the guest wires the file's own hoisted mocks, which cannot be imported here. +// Why the process identity: the session UA and app.userAgentFallback both carry it, so that is what a +// real guest's getUserAgent() reports until something writes the WebContents UA. export function createViewportGuestFactory( - mocks: BrowserManagerMocks + mocks: BrowserManagerMocks & { processUserAgent?: string } ): (id: number, url?: string) => ViewportGuestHandle { return function makeGuest(id: number, url = 'https://example.com/'): ViewportGuestHandle { const debuggerSendCommand = vi.fn().mockResolvedValue(undefined) const debuggerIsAttached = vi.fn(() => true) const debuggerAttach = vi.fn() - let currentUa = GUEST_ELECTRON_UA + let currentUa = mocks.processUserAgent ?? GUEST_ELECTRON_UA // Why: getURL() reports the last COMMITTED url — it does not move at did-start-navigation. let committedUrl = url + // Chromium applies CDP commands in issue order, so a later-issued write wins however they settle, + // and a rejected write changes nothing. An empty userAgent clears the override. + let issuedUserAgentWrites = 0 + let appliedUserAgentWrite = 0 + let cdpOverride: Record | null = null + const sendCommand = (method: string, params?: Record): unknown => { + const result: unknown = debuggerSendCommand(method, params) + if (method === 'Emulation.setUserAgentOverride') { + const ordinal = ++issuedUserAgentWrites + void Promise.resolve(result).then( + () => { + if (ordinal > appliedUserAgentWrite) { + appliedUserAgentWrite = ordinal + cdpOverride = params?.userAgent ? params : null + } + }, + () => {} + ) + } + return result + } const guest = { id, isDestroyed: vi.fn(() => false), @@ -53,7 +81,7 @@ export function createViewportGuestFactory( debugger: { isAttached: debuggerIsAttached, attach: debuggerAttach, - sendCommand: debuggerSendCommand, + sendCommand, on: vi.fn(), off: vi.fn() } @@ -68,7 +96,11 @@ export function createViewportGuestFactory( }, commitNavigationTo: (nextUrl: string) => { committedUrl = nextUrl - } + }, + webContentsUserAgent: () => currentUa, + presentedUserAgent: () => + typeof cdpOverride?.userAgent === 'string' ? cdpOverride.userAgent : currentUa, + standingUserAgentOverride: () => cdpOverride } } } diff --git a/src/main/browser/browser-manager-viewport.ts b/src/main/browser/browser-manager-viewport.ts index a4e263dcced..b85e6d0424f 100644 --- a/src/main/browser/browser-manager-viewport.ts +++ b/src/main/browser/browser-manager-viewport.ts @@ -5,9 +5,7 @@ import { 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' -import { getBrowserProcessUserAgentIdentity } from './browser-process-user-agent' 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. @@ -39,11 +37,11 @@ export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifec // 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, { + // Record the request before CDP runs: host panning and the tab's identity both follow it, so a + // navigation mid-apply already sees it. The guest id fence keeps it off a replacement guest. + this.viewportPresetByTabId.set(browserTabId, { guestWebContentsId: expectedWebContentsId, - active: override !== null + override }) } // The renderer resizes the host before CDP completes; discard the old geometry until it @@ -146,84 +144,54 @@ export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifec } const dbg = guest.debugger + const metricsApplied = await this.runViewportEmulationStep( + browserTabId, + 'device metrics', + () => + override + ? dbg.sendCommand('Emulation.setDeviceMetricsOverride', { + width: override.width, + height: override.height, + deviceScaleFactor: override.deviceScaleFactor, + mobile: override.mobile + }) + : dbg.sendCommand('Emulation.clearDeviceMetricsOverride', {}) + ) + const touchApplied = await this.runViewportEmulationStep(browserTabId, 'touch emulation', () => + dbg.sendCommand('Emulation.setTouchEmulationEnabled', { + enabled: override?.mobile ?? false, + maxTouchPoints: override?.mobile ? 5 : 0 + }) + ) + if (this.webContentsIdByTabId.get(browserTabId) !== webContentsId) { + return false + } + // Why: identity is not an emulation step. It follows the requested preset whatever the steps + // above did, so a failed metrics or touch write can never strand a mobile identity on the tab. + const identityApplied = await this.runViewportEmulationStep(browserTabId, 'identity', () => + this.retargetTabIdentity(guest, this.resolveTabNavigationUrl(guest)) + ) + return ( + metricsApplied && + touchApplied && + identityApplied && + this.webContentsIdByTabId.get(browserTabId) === webContentsId + ) + } + + private async runViewportEmulationStep( + browserTabId: string, + step: string, + send: () => Promise + ): Promise { 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 - }) - 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) - } 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 - }) - 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) - try { - if (this.authUserAgentOverrideStateByGuestId.has(guest.id)) { - const url = this.resolveTabNavigationUrl(guest) - const identity = getBrowserProcessUserAgentIdentity() - // Firefox is delivered per-target and cannot reach workers; keep it clean-only to preserve - // one coherent identity per mode instead of pairing a Firefox document with native workers. - const restored = await this.applyAuthUserAgentOverrideOverCdp( - guest, - false, - url, - identity.mode === 'clean' && isGoogleAuthUrl(url) - ? googleAuthUserAgent() - : identity.userAgent - ) - 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.webContentsIdByTabId.get(browserTabId) === webContentsId - ) { - this.viewportUaOverrideMobileByTabId.set(browserTabId, trackedMobile) - } - throw error - } - } - if (this.webContentsIdByTabId.get(browserTabId) !== webContentsId) { - return false - } - return true - } catch { + // A step that reports its own failure resolves false rather than throwing. + return (await send()) !== false + } catch (error) { + console.warn(`[browser-manager] setViewportOverride: ${step} failed`, { + browserTabId, + error: error instanceof Error ? error.message : String(error) + }) return false } } diff --git a/src/main/browser/browser-session-registry.test.ts b/src/main/browser/browser-session-registry.test.ts index 27965a56433..a3e84b290f0 100644 --- a/src/main/browser/browser-session-registry.test.ts +++ b/src/main/browser/browser-session-registry.test.ts @@ -556,7 +556,7 @@ describe('BrowserSessionRegistry', () => { { webRequest: { onBeforeSendHeaders } } as never, (request) => request.currentUserAgent === googleAuthUserAgent() - ? { userAgent: googleAuthUserAgent() } + ? { kind: 'google-auth', userAgent: googleAuthUserAgent() } : undefined ) expect(onBeforeSendHeaders).toHaveBeenCalledWith( diff --git a/src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts b/src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts index 05331ab5c65..c6cba96c6d7 100644 --- a/src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts +++ b/src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts @@ -252,10 +252,10 @@ async function run() { if (arm === 'branch') { installBrowserSessionUserAgentPolicy(sess, request => { if (request.resourceType !== 'mainFrame' && (request.currentUserAgent === firefoxUserAgent || request.effectiveUserAgent === firefoxUserAgent)) { - return { userAgent: firefoxUserAgent } + return { kind: 'google-auth', userAgent: firefoxUserAgent } } if (request.resourceType === 'mainFrame' && request.currentUserAgent === firefoxUserAgent) { - return { userAgent: cleanUserAgent } + return { kind: 'process', userAgent: cleanUserAgent } } return undefined }) diff --git a/src/main/browser/browser-session-ua-wire-probe-server.ts b/src/main/browser/browser-session-ua-wire-probe-server.ts index d00117ad86b..6062ed3df91 100644 --- a/src/main/browser/browser-session-ua-wire-probe-server.ts +++ b/src/main/browser/browser-session-ua-wire-probe-server.ts @@ -156,7 +156,7 @@ function probePage( const dedicatedWorkerScriptText = crossContext ? `const dedicatedDone = message('dedicated-worker'); const dedicated = new Worker(${JSON.stringify(httpOrigin)} + '/dedicated-worker.js'); dedicated.onmessage = event => postMessage(event.data, '*')` : '' - return `UA wire probe` + ` } function childPage( context: string, diff --git a/src/main/browser/browser-session-ua.ts b/src/main/browser/browser-session-ua.ts index 5b4d4735dcd..fc05fe357cc 100644 --- a/src/main/browser/browser-session-ua.ts +++ b/src/main/browser/browser-session-ua.ts @@ -1,5 +1,5 @@ import type { Session } from 'electron' -import type { ViewportUserAgentOverride } from './browser-viewport-user-agent' +import type { BrowserTabIdentity, UserAgentMetadata } from './browser-tab-identity' export { cleanElectronUserAgent } from './browser-process-user-agent' import { getBrowserProcessUserAgentIdentity } from './browser-process-user-agent' @@ -19,7 +19,7 @@ export type BrowserSessionRequestUserAgentResolver = (args: { webContentsId?: number currentUserAgent?: string effectiveUserAgent?: string -}) => ViewportUserAgentOverride | undefined +}) => BrowserTabIdentity | undefined function quoteClientHint(value: string): string { return `"${value.replace(/["\\]/g, '\\$&')}"` @@ -33,7 +33,7 @@ function formatClientHintBrands(brands: { brand: string; version: string }[]): s function applyUserAgentMetadataHeaders( headers: Record, - metadata: NonNullable + metadata: UserAgentMetadata ): void { const values: Record = { 'sec-ch-ua': formatClientHintBrands(metadata.brands), @@ -101,15 +101,10 @@ export function installBrowserSessionUserAgentPolicy( callback({ requestHeaders: headers }) return } - if (identity.userAgent) { - setUserAgentHeader(headers, identity.userAgent) - } - if (identity.userAgent === firefoxUa) { + setUserAgentHeader(headers, identity.userAgent) + if (identity.kind === 'google-auth') { stripClientHints(headers) - callback({ requestHeaders: headers }) - return - } - if (identity.userAgentMetadata) { + } else if (identity.kind === 'mobile') { applyUserAgentMetadataHeaders(headers, identity.userAgentMetadata) } callback({ requestHeaders: headers }) diff --git a/src/main/browser/browser-tab-identity.test.ts b/src/main/browser/browser-tab-identity.test.ts new file mode 100644 index 00000000000..520eae50819 --- /dev/null +++ b/src/main/browser/browser-tab-identity.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest' + +import { googleAuthUserAgent } from './browser-google-auth-ua' +import { resolveBrowserTabIdentity } from './browser-tab-identity' + +const CHROME_UA = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' +const NATIVE_UA = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Orca/1.0.0 Chrome/134.0.0.0 Electron/43.0.0 Safari/537.36' +const clean = { mode: 'clean', userAgent: CHROME_UA } as const +const native = { mode: 'native', userAgent: NATIVE_UA } as const +const AUTH_URL = 'https://accounts.google.com/v3/signin/identifier' + +describe('resolveBrowserTabIdentity', () => { + it('presents Firefox on Google auth hosts in clean mode regardless of the preset', () => { + for (const mobile of [false, true]) { + expect(resolveBrowserTabIdentity({ url: AUTH_URL, mobile, processIdentity: clean })).toEqual({ + kind: 'google-auth', + userAgent: googleAuthUserAgent() + }) + } + }) + + it('leaves Google auth hosts on the native identity in native mode', () => { + expect( + resolveBrowserTabIdentity({ url: AUTH_URL, mobile: false, processIdentity: native }) + ).toEqual({ kind: 'process', userAgent: NATIVE_UA }) + expect( + resolveBrowserTabIdentity({ url: AUTH_URL, mobile: true, processIdentity: native }).kind + ).toBe('mobile') + }) + + it('presents the process identity itself off the auth hosts without a mobile preset', () => { + for (const processIdentity of [clean, native]) { + expect( + resolveBrowserTabIdentity({ + url: 'https://myaccount.google.com/', + mobile: false, + processIdentity + }) + ).toEqual({ kind: 'process', userAgent: processIdentity.userAgent }) + } + }) + + it('splices the real Chrome major into the mobile UA and its client hints', () => { + const identity = resolveBrowserTabIdentity({ + url: 'https://example.com/', + mobile: true, + processIdentity: clean + }) + expect(identity.kind).toBe('mobile') + expect(identity.userAgent).toContain('iPhone') + expect(identity.userAgent).toContain('CriOS/134.0.0.0') + if (identity.kind === 'mobile') { + expect(identity.userAgentMetadata.mobile).toBe(true) + expect(identity.userAgentMetadata.platform).toBe('iOS') + expect(identity.userAgentMetadata.brands).toContainEqual({ + brand: 'Google Chrome', + version: '134' + }) + } + }) + + it('falls back to a known Chrome major when the process UA carries none', () => { + const identity = resolveBrowserTabIdentity({ + url: 'https://example.com/', + mobile: true, + processIdentity: { mode: 'clean', userAgent: googleAuthUserAgent() } + }) + expect(identity.userAgent).toContain('CriOS/134.0.0.0') + }) + + it('treats an unparseable URL as a non-auth host', () => { + expect( + resolveBrowserTabIdentity({ url: 'not a url', mobile: false, processIdentity: clean }) + ).toEqual({ kind: 'process', userAgent: CHROME_UA }) + }) +}) diff --git a/src/main/browser/browser-tab-identity.ts b/src/main/browser/browser-tab-identity.ts new file mode 100644 index 00000000000..713b0310a4a --- /dev/null +++ b/src/main/browser/browser-tab-identity.ts @@ -0,0 +1,90 @@ +// Why one owner: a tab's identity is read by the session request hook and written to the guest over +// two layers (the WebContents UA and a CDP override that outranks it). When each layer derived it on +// its own, a desktop viewport preset installed a CDP override with no userAgentMetadata, and Chromium +// then drops navigator.userAgentData and every sec-ch-ua header for that tab — a Chrome UA with no +// client hints, which bot checks read as a spoof. Every layer now asks this module instead. + +import type { BrowserProcessUserAgentIdentity } from './browser-process-user-agent' +import { googleAuthUserAgent, isGoogleAuthUrl } from './browser-google-auth-ua' + +type UserAgentBrand = { brand: string; version: string } + +export type UserAgentMetadata = { + brands: UserAgentBrand[] + fullVersionList: UserAgentBrand[] + fullVersion: string + platform: string + platformVersion: string + architecture: string + model: string + mobile: boolean +} + +export type BrowserTabIdentity = + /** The process identity, with its client hints left to Chromium. */ + | { kind: 'process'; userAgent: string } + /** Firefox on Google's auth hosts; real Firefox sends no client hints. */ + | { kind: 'google-auth'; userAgent: string } + | { kind: 'mobile'; userAgent: string; userAgentMetadata: UserAgentMetadata } + +export function googleAuthTabIdentity(): BrowserTabIdentity { + return { kind: 'google-auth', userAgent: googleAuthUserAgent() } +} + +/** + * The identity a tab presents at `url`. Desktop presets and "no preset" are deliberately the same + * input: only a mobile preset changes who the tab claims to be. + */ +export function resolveBrowserTabIdentity(args: { + url: string + mobile: boolean + processIdentity: BrowserProcessUserAgentIdentity +}): BrowserTabIdentity { + // Firefox is delivered per-target and cannot reach workers; keep it clean-only to preserve one + // coherent identity per mode instead of pairing a Firefox document with native workers. + if (args.processIdentity.mode === 'clean' && isGoogleAuthUrl(args.url)) { + return googleAuthTabIdentity() + } + if (args.mobile) { + return buildMobileTabIdentity(args.processIdentity.userAgent) + } + return { kind: 'process', userAgent: args.processIdentity.userAgent } +} + +// Why: responsive sites UA-sniff; this is Chrome DevTools' default iPhone UA template with the real +// Chrome major spliced in so the userAgentMetadata brands below agree with it. +function buildMobileUserAgent(chromeMajor: string): string { + return `Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/${chromeMajor}.0.0.0 Mobile/15E148 Safari/604.1` +} + +function extractChromeMajor(ua: string): string { + const match = ua.match(/Chrome\/(\d+)/) + return match ? match[1] : '134' +} + +function buildMobileTabIdentity(processUserAgent: string): BrowserTabIdentity { + const chromeMajor = extractChromeMajor(processUserAgent) + // Why: userAgentMetadata must accompany the mobile UA so client hints match, or bot-detection flags the desktop-hint leak. + return { + kind: 'mobile', + userAgent: buildMobileUserAgent(chromeMajor), + userAgentMetadata: { + brands: [ + { brand: 'Google Chrome', version: chromeMajor }, + { brand: 'Chromium', version: chromeMajor }, + { brand: 'Not/A)Brand', version: '24' } + ], + fullVersionList: [ + { brand: 'Google Chrome', version: `${chromeMajor}.0.0.0` }, + { brand: 'Chromium', version: `${chromeMajor}.0.0.0` }, + { brand: 'Not/A)Brand', version: '24.0.0.0' } + ], + fullVersion: `${chromeMajor}.0.0.0`, + platform: 'iOS', + platformVersion: '17.0', + architecture: '', + model: 'iPhone', + mobile: true + } + } +} diff --git a/src/main/browser/browser-viewport-user-agent.test.ts b/src/main/browser/browser-viewport-user-agent.test.ts deleted file mode 100644 index 1d069a209ad..00000000000 --- a/src/main/browser/browser-viewport-user-agent.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { googleAuthUserAgent } from './browser-google-auth-ua' -import { buildViewportUserAgentOverride } from './browser-viewport-user-agent' - -const CHROME_UA = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' - -describe('buildViewportUserAgentOverride', () => { - it('presents the Firefox UA on Google auth hosts regardless of the preset', () => { - for (const mobile of [false, true]) { - const override = buildViewportUserAgentOverride({ - url: 'https://accounts.google.com/v3/signin/identifier', - mobile, - baseUserAgent: CHROME_UA - }) - expect(override.userAgent).toBe(googleAuthUserAgent()) - // Real Firefox emits no client hints, so Chrome brands would contradict the stripped headers. - expect(override.userAgentMetadata).toBeUndefined() - } - }) - - it('keeps the clean desktop UA off the auth hosts', () => { - const override = buildViewportUserAgentOverride({ - url: 'https://myaccount.google.com/', - mobile: false, - baseUserAgent: CHROME_UA - }) - expect(override.userAgent).toBe(CHROME_UA) - expect(override.userAgentMetadata).toBeUndefined() - }) - - it('splices the real Chrome major into the mobile UA and its client hints', () => { - const override = buildViewportUserAgentOverride({ - url: 'https://example.com/', - mobile: true, - baseUserAgent: CHROME_UA - }) - expect(override.userAgent).toContain('iPhone') - expect(override.userAgent).toContain('CriOS/134.0.0.0') - expect(override.userAgentMetadata?.mobile).toBe(true) - expect(override.userAgentMetadata?.platform).toBe('iOS') - expect(override.userAgentMetadata?.brands).toContainEqual({ - brand: 'Google Chrome', - version: '134' - }) - }) - - it('falls back to a known Chrome major when the base UA carries none', () => { - const override = buildViewportUserAgentOverride({ - url: 'https://example.com/', - mobile: true, - baseUserAgent: googleAuthUserAgent() - }) - expect(override.userAgent).toContain('CriOS/134.0.0.0') - }) - - it('treats an unparseable URL as a non-auth host', () => { - const override = buildViewportUserAgentOverride({ - url: 'not a url', - mobile: false, - baseUserAgent: CHROME_UA - }) - expect(override.userAgent).toBe(CHROME_UA) - }) -}) diff --git a/src/main/browser/browser-viewport-user-agent.ts b/src/main/browser/browser-viewport-user-agent.ts deleted file mode 100644 index 31c06307b46..00000000000 --- a/src/main/browser/browser-viewport-user-agent.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Why: a CDP Emulation.setUserAgentOverride outranks WebContents.setUserAgent for both -// navigator.userAgent and the outgoing request header, and it stands across every later -// navigation until explicitly cleared. So the viewport preset's UA is a third identity layer -// that must agree with the auth-host Firefox switch, or applying a preset silently reintroduces -// the exact UA mismatch this scope exists to remove. - -import { googleAuthUserAgent, isGoogleAuthUrl } from './browser-google-auth-ua' - -type UserAgentBrand = { brand: string; version: string } - -export type ViewportUserAgentOverride = { - userAgent: string - userAgentMetadata?: { - brands: UserAgentBrand[] - fullVersionList: UserAgentBrand[] - fullVersion: string - platform: string - platformVersion: string - architecture: string - model: string - mobile: boolean - } -} - -// Why: responsive sites UA-sniff; this is Chrome DevTools' default iPhone UA template with the real -// Chrome major spliced in so the userAgentMetadata brands below agree with it. -function buildMobileUserAgent(chromeMajor: string): string { - return `Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/${chromeMajor}.0.0.0 Mobile/15E148 Safari/604.1` -} - -function extractChromeMajor(ua: string): string { - const match = ua.match(/Chrome\/(\d+)/) - return match ? match[1] : '134' -} - -export function buildViewportUserAgentOverride(args: { - url: string - mobile: boolean - baseUserAgent: string - googleAuthEnabled?: boolean -}): ViewportUserAgentOverride { - if (args.googleAuthEnabled !== false && isGoogleAuthUrl(args.url)) { - // Why: match the header-level Firefox switch exactly, and send no userAgentMetadata — real - // Firefox emits no client hints, so Chrome brands here would contradict the stripped headers. - return { userAgent: googleAuthUserAgent() } - } - if (!args.mobile) { - // Why: desktop presets republish the session's clean identity, or a preset would put the - // Electron/app tokens back on the wire and a transplanted session gets revoked (STA-7147). - return { userAgent: args.baseUserAgent } - } - const chromeMajor = extractChromeMajor(args.baseUserAgent) - // Why: userAgentMetadata must accompany the mobile UA so client hints match, or bot-detection flags the desktop-hint leak. - return { - userAgent: buildMobileUserAgent(chromeMajor), - userAgentMetadata: { - brands: [ - { brand: 'Google Chrome', version: chromeMajor }, - { brand: 'Chromium', version: chromeMajor }, - { brand: 'Not/A)Brand', version: '24' } - ], - fullVersionList: [ - { brand: 'Google Chrome', version: `${chromeMajor}.0.0.0` }, - { brand: 'Chromium', version: `${chromeMajor}.0.0.0` }, - { brand: 'Not/A)Brand', version: '24.0.0.0' } - ], - fullVersion: `${chromeMajor}.0.0.0`, - platform: 'iOS', - platformVersion: '17.0', - architecture: '', - model: 'iPhone', - mobile: true - } - } -} diff --git a/src/main/claude/__fixtures__/claude-adapter-capture-early-steer.jsonl b/src/main/claude/__fixtures__/claude-adapter-capture-early-steer.jsonl new file mode 100644 index 00000000000..bff60192d74 --- /dev/null +++ b/src/main/claude/__fixtures__/claude-adapter-capture-early-steer.jsonl @@ -0,0 +1,68 @@ +{"at": 0, "kind": "meta", "scenario": "early-steer", "providerSessionId": "00000000-0000-4000-8000-00000000c1a0", "note": "Recorded through Orca adapter vs Claude CLI 2.1.280; scrubbed: session id, home/tmp paths."} +{"at": 219, "kind": "frame", "frame": {"type": "system", "subtype": "hook_started", "hook_id": "e13780eb-2f2b-45e5-a9fd-e7868ef6d9e2", "hook_name": "SessionStart:startup", "hook_event": "SessionStart", "uuid": "308e42c2-3222-452b-bb5f-f8e41cf485a4", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 229, "kind": "frame", "frame": {"type": "system", "subtype": "hook_response", "hook_id": "e13780eb-2f2b-45e5-a9fd-e7868ef6d9e2", "hook_name": "SessionStart:startup", "hook_event": "SessionStart", "output": "\n", "stdout": "\n", "stderr": "", "exit_code": 0, "outcome": "success", "uuid": "8c37d862-0ba3-4e14-a746-68a29d137384", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 242, "kind": "dispatch", "clientMessageId": "client-A", "sentUuid": "c64cdf14-1349-4be1-b0a4-5613890d57bd", "text": "Use the Bash tool to run `sleep 8` two separate times, one call at a time, waiting for each. Then reply exactly: FIRST DONE", "outcome": {"state": "admitted"}} +{"at": 244, "kind": "frame", "frame": {"type": "system", "subtype": "session_state_changed", "state": "running", "uuid": "052ca984-de90-4289-ac97-587d361f35dc", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 244, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "c64cdf14-1349-4be1-b0a4-5613890d57bd", "state": "queued", "uuid": "e32cf6db-6c39-49ad-87a6-ad8f392e00b1", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 244, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "c64cdf14-1349-4be1-b0a4-5613890d57bd", "state": "started", "uuid": "0c7a74e6-8f5e-43c4-9cf0-af6e3fac6e0f", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 259, "kind": "frame", "frame": {"type": "system", "subtype": "init", "cwd": "/tmp/scrubbed-cwd", "session_id": "00000000-0000-4000-8000-00000000c1a0", "tools": ["Task", "AskUserQuestion", "Bash", "CronCreate", "CronDelete", "CronList", "DesignSync", "Edit", "EnterPlanMode", "EnterWorktree", "ExitPlanMode", "ExitWorktree", "ListAgents", "NotebookEdit", "Read", "ReportFindings", "ScheduleWakeup", "SendMessage", "Skill", "TaskCreate", "TaskGet", "TaskList", "TaskStop", "TaskUpdate", "ToolSearch", "WebFetch", "WebSearch", "Workflow", "Write"], "mcp_servers": [], "model": "claude-opus-5-5[1m]", "permissionMode": "default", "slash_commands": ["deep-research", "design", "design-sync", "dataviz", "update-config", "verify", "debug", "code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop", "claude-api", "workflow-authoring", "run", "run-skill-generator", "agents", "auto-mode-setup", "autocompact", "clear", "color", "compact", "config", "output-style", "context", "effort", "fast", "heapdump", "init", "mcp", "model", "__remote-workflow", "workflow-launch-exec", "reload-plugins", "reload-skills", "rename", "security-review", "usage", "insights", "recap", "goal", "design-consent", "design-revoke", "list-agents", "team-onboarding"], "terminal_slash_commands": ["doctor", "color", "reload-plugins"], "apiKeySource": "none", "claude_code_version": "2.1.280", "output_style": "default", "agents": ["claude", "Explore", "general-purpose", "Plan", "statusline-setup"], "skills": ["deep-research", "design", "design-sync", "dataviz", "update-config", "verify", "debug", "code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop", "claude-api", "workflow-authoring", "run", "run-skill-generator"], "plugins": [{"name": "telemetry", "path": "builtin", "source": "telemetry@builtin"}], "capabilities": ["interrupt_receipt_v1", "interrupt_cancel_queued_v1", "msg_lifecycle_v1", "mcp_read_resource_v1", "mcp_tool_ui_meta_v1"], "analytics_disabled": false, "product_feedback_disabled": false, "uuid": "ff6bb8ec-afcb-4bb8-88a8-d75625377ab9", "memory_paths": {"auto": "/Users/user/scrubbed"}, "messaging_socket_path": "/tmp/scrubbed.sock", "fast_mode_state": "off", "fast_mode_disabled_reason": "sdk_opt_in_required"}} +{"at": 261, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "9900a74e-78ff-48a6-b005-8b748a8f7a1d"}} +{"at": 492, "kind": "dispatch", "clientMessageId": "client-B", "sentUuid": "3a4cf7ae-5121-45d4-8c99-76625c847c4e", "text": "Also say the word banana at the end of your reply.", "outcome": {"state": "admitted"}} +{"at": 493, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "3a4cf7ae-5121-45d4-8c99-76625c847c4e", "state": "queued", "uuid": "2244a4b7-a832-418a-b6e9-be4657618f60", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 1342, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "Use the Bash tool to run `sleep 8` two separate times, one call at a time, waiting for each. Then reply exactly: FIRST DONE"}]}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "c64cdf14-1349-4be1-b0a4-5613890d57bd", "timestamp": "2026-09-28T12:50:50.211Z", "isReplay": true}} +{"at": 1342, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsEi9RgiS61CeGuiyaP", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 5242, "cache_read_input_tokens": 11514, "cache_creation": {"ephemeral_5m_input_tokens": 5242, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "25b0f48f-8de3-4e58-a53f-76cedc10c4e8", "ttft_ms": 1075, "user_message_uuid": "c64cdf14-1349-4be1-b0a4-5613890d57bd", "user_message_uuids": ["c64cdf14-1349-4be1-b0a4-5613890d57bd"]}} +{"at": 1342, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_01RL5fbYfvXaRva9PBe2C7Qn", "name": "Bash", "input": {}, "caller": {"type": "direct"}}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "54809bd1-bb5a-4fe7-9a04-1dc60e5a7163"}} +{"at": 1342, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "02a681bb-01c9-4dd3-b890-040e17bf4132"}} +{"at": 2307, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"com"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "0a1147d0-c168-405a-a4e4-79ad5266d3c2"}} +{"at": 2307, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "mand\": \"s"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "1eabfcb6-b0c4-403a-9804-c55b013391ae"}} +{"at": 2307, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "lee"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "20f34e40-6392-4e4a-9991-ad35ad6b1aaa"}} +{"at": 2309, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "p 8\""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ec8ed822-0e8f-46c1-b7a0-c8ab4467baa9"}} +{"at": 2330, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ", \"descri"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "87881476-c659-4e82-945d-be15eaa45501"}} +{"at": 2332, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ption\": \""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "e2502f3a-3cca-4741-9df5-dade6f4e4abb"}} +{"at": 2332, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "Sl"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "dc28f0f9-22f5-47fc-a374-1369c6fda079"}} +{"at": 2335, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "eep f"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "85345f83-b80d-4142-aa66-66b8640dca3b"}} +{"at": 2340, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "or 8 "}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "5ce61d4a-abd9-47bd-9c7b-e01a6d566387"}} +{"at": 2341, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "seconds\"}"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "21de6d37-199b-44d7-bfaa-4af1471a7235"}} +{"at": 2343, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsEi9RgiS61CeGuiyaP", "type": "message", "role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01RL5fbYfvXaRva9PBe2C7Qn", "name": "Bash", "input": {"command": "sleep 8", "description": "Sleep for 8 seconds"}, "caller": {"type": "direct"}}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 5242, "cache_read_input_tokens": 11514, "cache_creation": {"ephemeral_5m_input_tokens": 5242, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "3f3c0fff-9ca6-49f9-8b73-36e569af5a38", "timestamp": "2026-09-28T12:50:52.296Z", "request_id": "req_011CfVsEhBPEiAFbbAE18svX", "user_message_uuid": "c64cdf14-1349-4be1-b0a4-5613890d57bd", "user_message_uuids": ["c64cdf14-1349-4be1-b0a4-5613890d57bd"]}} +{"at": 2347, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "fa196eb6-011b-4fa7-831d-4ffe2582a3c7"}} +{"at": 2375, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "tool_use", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 5242, "cache_read_input_tokens": 11514, "output_tokens": 82, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "7b300bd2-eaa1-4cb7-a557-6908cbb6f2a6"}} +{"at": 2380, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "c1573618-e6fb-4f22-8939-e7959440f18a"}} +{"at": 5482, "kind": "frame", "frame": {"type": "system", "subtype": "task_started", "task_id": "bys65yjga", "tool_use_id": "toolu_01RL5fbYfvXaRva9PBe2C7Qn", "description": "Sleep for 8 seconds", "is_backgrounded": false, "task_type": "local_bash", "uuid": "6b741fb3-6edc-4c9e-a69a-81fc87ef1502", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 10493, "kind": "frame", "frame": {"type": "system", "subtype": "task_notification", "task_id": "bys65yjga", "tool_use_id": "toolu_01RL5fbYfvXaRva9PBe2C7Qn", "status": "completed", "output_file": "", "summary": "Sleep for 8 seconds", "uuid": "9b0edc57-cd0c-4f60-a72c-e359e1a7fabc", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 10496, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"tool_use_id": "toolu_01RL5fbYfvXaRva9PBe2C7Qn", "type": "tool_result", "content": "(Bash completed with no output)", "is_error": false}]}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "2f7a468f-55f1-4434-8f68-70a077f74f6e", "timestamp": "2026-09-28T12:51:00.450Z", "tool_use_result": {"stdout": "", "stderr": "", "interrupted": false, "isImage": false, "noOutputExpected": false}}} +{"at": 10499, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "Also say the word banana at the end of your reply."}]}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "3a4cf7ae-5121-45d4-8c99-76625c847c4e", "timestamp": "2026-09-28T12:50:50.448Z", "isReplay": true}} +{"at": 10499, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "3a4cf7ae-5121-45d4-8c99-76625c847c4e", "state": "started", "uuid": "aca666d1-c3b3-451e-81d7-5aadfa246b42", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 10499, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "ee6f2da9-2514-4086-8bc4-1a5c01e5207b"}} +{"at": 11452, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsFTNDFHDiwBZc8cKWk", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 217, "cache_read_input_tokens": 16756, "cache_creation": {"ephemeral_5m_input_tokens": 217, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "571285ce-eda5-44b5-818c-db549ee9aef9", "ttft_ms": 949}} +{"at": 11452, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_01HG7iwC2HgvoxndbuytmJCM", "name": "Bash", "input": {}, "caller": {"type": "direct"}}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "6d6d5ecd-0889-4592-b514-d72c0a964cc9"}} +{"at": 11453, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "58339280-14b3-4f61-bd2b-726418d4b324"}} +{"at": 11853, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"comma"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "37c5f88b-3ef0-471c-89b1-a8dc6f3f61ad"}} +{"at": 11866, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "nd\": \"s"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "f31aa8f4-903d-4a7d-aeff-41c16f4b487d"}} +{"at": 11866, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "leep 8\""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "b1b59a9c-cec2-4398-9e8e-e49f16ad712d"}} +{"at": 11882, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ", \"desc"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "25d130db-a0f9-43d2-b639-6a9064f1e00d"}} +{"at": 11883, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ription"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "e0b776a6-7cb3-4512-bacc-07fc84a1737e"}} +{"at": 11883, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "\": \"Sle"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "54ec71a5-84ad-4f45-849f-cdbeae44d781"}} +{"at": 11883, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ep for "}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "667d1090-ceca-4c88-9864-447d6c95784e"}} +{"at": 11883, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "8 secon"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "2047e8e3-feb9-484d-8a1a-c19f0afebd59"}} +{"at": 11883, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ds\"}"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "7d4f9da1-d974-4acf-84a5-d1f22068beec"}} +{"at": 11884, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsFTNDFHDiwBZc8cKWk", "type": "message", "role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01HG7iwC2HgvoxndbuytmJCM", "name": "Bash", "input": {"command": "sleep 8", "description": "Sleep for 8 seconds"}, "caller": {"type": "direct"}}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 217, "cache_read_input_tokens": 16756, "cache_creation": {"ephemeral_5m_input_tokens": 217, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "6c4c5b57-e219-4c0d-a90f-26cbae8a33df", "timestamp": "2026-09-28T12:51:01.838Z", "request_id": "req_011CfVsFSwuBKQ87EU42vew5"}} +{"at": 11885, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "df9289e5-af2a-4b85-8dc8-47df67725383"}} +{"at": 11885, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "tool_use", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 217, "cache_read_input_tokens": 16756, "output_tokens": 82, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "7ac75599-4149-466f-ab0b-4d4ef74a9c86"}} +{"at": 11885, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ea092344-1e27-4b3f-9145-e388a268a4a1"}} +{"at": 14890, "kind": "frame", "frame": {"type": "system", "subtype": "task_started", "task_id": "bu917pf4q", "tool_use_id": "toolu_01HG7iwC2HgvoxndbuytmJCM", "description": "Sleep for 8 seconds", "is_backgrounded": false, "task_type": "local_bash", "uuid": "408f8301-dbd6-43cb-8f09-7f7d5f4325da", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 19903, "kind": "frame", "frame": {"type": "system", "subtype": "task_notification", "task_id": "bu917pf4q", "tool_use_id": "toolu_01HG7iwC2HgvoxndbuytmJCM", "status": "completed", "output_file": "", "summary": "Sleep for 8 seconds", "uuid": "55ee40ae-c83e-40de-ae7c-fb0659950b39", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 19906, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"tool_use_id": "toolu_01HG7iwC2HgvoxndbuytmJCM", "type": "tool_result", "content": "(Bash completed with no output)", "is_error": false}]}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "ee783948-1a20-4797-b0ef-b191babd6a68", "timestamp": "2026-09-28T12:51:09.859Z", "tool_use_result": {"stdout": "", "stderr": "", "interrupted": false, "isImage": false, "noOutputExpected": false}}} +{"at": 19908, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "dec2fc2c-f27f-44c2-bbda-01b112f803d6"}} +{"at": 21540, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsGBuJTPMNVKzDFTw9k", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 121, "cache_read_input_tokens": 16973, "cache_creation": {"ephemeral_5m_input_tokens": 121, "ephemeral_1h_input_tokens": 0}, "output_tokens": 11, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "14cb8df8-de6e-43e8-a0be-46c883c88501", "ttft_ms": 1627}} +{"at": 21540, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "f346ec9e-fe33-4c2e-a611-b2da26f43f53"}} +{"at": 21540, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "FIRST DONE ban"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "6440a3c0-de10-4e9e-a898-7ef0ec79673c"}} +{"at": 21540, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "ana"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "7096cb35-c640-4698-b755-8aad289d6cef"}} +{"at": 21556, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsGBuJTPMNVKzDFTw9k", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "FIRST DONE banana"}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 121, "cache_read_input_tokens": 16973, "cache_creation": {"ephemeral_5m_input_tokens": 121, "ephemeral_1h_input_tokens": 0}, "output_tokens": 11, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "5c9408ee-fcd2-46e8-aca0-1e2e8563420c", "timestamp": "2026-09-28T12:51:11.495Z", "request_id": "req_011CfVsG9F4VXdbez6Yd6wis"}} +{"at": 21556, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "12aa2a40-1da6-45bc-bd9e-a1f999c144d2"}} +{"at": 21556, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 121, "cache_read_input_tokens": 16973, "output_tokens": 11, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "fee3cade-9b96-4f0e-8657-b23a6c6f83ca"}} +{"at": 21557, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "b39aedff-1723-46d2-8fbf-d62b2fe8f2ac"}} +{"at": 21559, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "3a4cf7ae-5121-45d4-8c99-76625c847c4e", "state": "completed", "uuid": "5b6a0e7a-361d-42ae-983a-5ecc00d6ff8c", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 21562, "kind": "frame", "frame": {"duration_api_ms": 6682, "stop_reason": "end_turn", "session_id": "00000000-0000-4000-8000-00000000c1a0", "total_cost_usd": 0.045744599999999996, "usage": {"input_tokens": 6, "cache_creation_input_tokens": 5580, "cache_read_input_tokens": 45243, "output_tokens": 175, "output_tokens_details": {"thinking_tokens": 0}, "server_tool_use": {"web_search_requests": 0, "web_fetch_requests": 0}, "service_tier": "standard", "cache_creation": {"ephemeral_1h_input_tokens": 0, "ephemeral_5m_input_tokens": 5580}, "inference_geo": "not_available", "iterations": [], "speed": "standard"}, "modelUsage": {"claude-opus-5-5[1m]": {"inputTokens": 1219, "outputTokens": 196, "cacheReadInputTokens": 45243, "cacheCreationInputTokens": 5580, "webSearchRequests": 0, "costUSD": 0.045744599999999996, "contextWindow": 1000000, "maxOutputTokens": 128000, "thinkingTokens": 0, "canonicalModel": "claude-opus-5-5", "provider": "firstParty", "costBasis": "list"}}, "permission_denials": [], "terminal_reason": "completed", "fast_mode_state": "off", "fast_mode_disabled_reason": "sdk_opt_in_required", "subagent_stats": {"spawned": 0, "requested": {"background": 0, "foreground": 0, "unset": 0}, "started_in_background": 0, "max_depth": 0, "spawned_by_subagents": 0, "completed": 0, "failed": 0, "killed": {"parent": 0, "user": 0, "system": 0}, "refused": {"depth_limit": 0, "concurrency_limit": 0, "budget": 0}, "by_type": {}}, "is_error": false, "num_turns": 3, "subtype": "success", "api_error_status": null, "result": "FIRST DONE banana", "ttft_ms": 2096, "type": "result", "duration_ms": 21314, "uuid": "02543cfd-4942-47f9-82d7-0bd79a2ed93d", "ttft_stream_ms": 1096, "time_to_request_ms": 21, "first_content_frame_ms": 1096, "user_message_uuid": "c64cdf14-1349-4be1-b0a4-5613890d57bd", "user_message_uuids": ["c64cdf14-1349-4be1-b0a4-5613890d57bd", "3a4cf7ae-5121-45d4-8c99-76625c847c4e"], "request_sent_wall_ms": 1790599850221, "queued_turn_count": 0, "result_index": 0}} +{"at": 21563, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "c64cdf14-1349-4be1-b0a4-5613890d57bd", "state": "completed", "uuid": "ef4b1bcb-e9cf-42f0-a0f9-b25a59a56efe", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 21563, "kind": "frame", "frame": {"type": "system", "subtype": "session_state_changed", "state": "idle", "uuid": "fce05c2c-6c43-4c7d-a347-ec3e378e3afc", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 24574, "kind": "end", "results": 1} diff --git a/src/main/claude/__fixtures__/claude-adapter-capture-fold.jsonl b/src/main/claude/__fixtures__/claude-adapter-capture-fold.jsonl new file mode 100644 index 00000000000..5bf4b2f863c --- /dev/null +++ b/src/main/claude/__fixtures__/claude-adapter-capture-fold.jsonl @@ -0,0 +1,65 @@ +{"at": 1, "kind": "meta", "scenario": "fold", "providerSessionId": "00000000-0000-4000-8000-00000000c1a0", "note": "Recorded through Orca adapter vs Claude CLI 2.1.280; scrubbed: session id, home/tmp paths."} +{"at": 242, "kind": "frame", "frame": {"type": "system", "subtype": "hook_started", "hook_id": "e8d2b9c8-d0d0-442f-9b86-580619257967", "hook_name": "SessionStart:startup", "hook_event": "SessionStart", "uuid": "fedf8819-07f0-41f5-bc48-24a71fa9940d", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 254, "kind": "frame", "frame": {"type": "system", "subtype": "hook_response", "hook_id": "e8d2b9c8-d0d0-442f-9b86-580619257967", "hook_name": "SessionStart:startup", "hook_event": "SessionStart", "output": "\n", "stdout": "\n", "stderr": "", "exit_code": 0, "outcome": "success", "uuid": "fed65947-fa85-4c2d-9bd6-1c01159922b4", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 269, "kind": "dispatch", "clientMessageId": "client-A", "sentUuid": "c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "text": "Use the Bash tool to run `sleep 8` two separate times, one call at a time, waiting for each. Then reply exactly: FIRST DONE", "outcome": {"state": "admitted"}} +{"at": 272, "kind": "frame", "frame": {"type": "system", "subtype": "session_state_changed", "state": "running", "uuid": "c36c2f2e-5c24-426d-819b-ea5f89862dae", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 272, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "state": "queued", "uuid": "65530cf1-9a29-4668-9a9f-91616c315809", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 272, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "state": "started", "uuid": "69d4df37-8379-4fce-bbf7-2a3a6854fbf2", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 287, "kind": "frame", "frame": {"type": "system", "subtype": "init", "cwd": "/tmp/scrubbed-cwd", "session_id": "00000000-0000-4000-8000-00000000c1a0", "tools": ["Task", "AskUserQuestion", "Bash", "CronCreate", "CronDelete", "CronList", "DesignSync", "Edit", "EnterPlanMode", "EnterWorktree", "ExitPlanMode", "ExitWorktree", "ListAgents", "NotebookEdit", "Read", "ReportFindings", "ScheduleWakeup", "SendMessage", "Skill", "TaskCreate", "TaskGet", "TaskList", "TaskStop", "TaskUpdate", "ToolSearch", "WebFetch", "WebSearch", "Workflow", "Write"], "mcp_servers": [], "model": "claude-opus-5-5[1m]", "permissionMode": "default", "slash_commands": ["deep-research", "design", "design-sync", "dataviz", "update-config", "verify", "debug", "code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop", "claude-api", "workflow-authoring", "run", "run-skill-generator", "agents", "auto-mode-setup", "autocompact", "clear", "color", "compact", "config", "output-style", "context", "effort", "fast", "heapdump", "init", "mcp", "model", "__remote-workflow", "workflow-launch-exec", "reload-plugins", "reload-skills", "rename", "security-review", "usage", "insights", "recap", "goal", "design-consent", "design-revoke", "list-agents", "team-onboarding"], "terminal_slash_commands": ["doctor", "color", "reload-plugins"], "apiKeySource": "none", "claude_code_version": "2.1.280", "output_style": "default", "agents": ["claude", "Explore", "general-purpose", "Plan", "statusline-setup"], "skills": ["deep-research", "design", "design-sync", "dataviz", "update-config", "verify", "debug", "code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop", "claude-api", "workflow-authoring", "run", "run-skill-generator"], "plugins": [{"name": "telemetry", "path": "builtin", "source": "telemetry@builtin"}], "capabilities": ["interrupt_receipt_v1", "interrupt_cancel_queued_v1", "msg_lifecycle_v1", "mcp_read_resource_v1", "mcp_tool_ui_meta_v1"], "analytics_disabled": false, "product_feedback_disabled": false, "uuid": "facd6d73-8d9e-4886-a6ab-f0873938128e", "memory_paths": {"auto": "/Users/user/scrubbed"}, "messaging_socket_path": "/tmp/scrubbed.sock", "fast_mode_state": "off", "fast_mode_disabled_reason": "sdk_opt_in_required"}} +{"at": 289, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "d34f89ba-f2ef-4cbf-99a5-7d4d85c8d02a"}} +{"at": 1527, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "Use the Bash tool to run `sleep 8` two separate times, one call at a time, waiting for each. Then reply exactly: FIRST DONE"}]}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "timestamp": "2026-09-28T12:50:24.512Z", "isReplay": true}} +{"at": 1528, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsCqF1QKvUNMHjZNCWW", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 16754, "cache_read_input_tokens": 0, "cache_creation": {"ephemeral_5m_input_tokens": 16754, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ac67ce6c-da49-4c1d-bf6a-70e2474ab35f", "ttft_ms": 1230, "user_message_uuid": "c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "user_message_uuids": ["c1a5941b-1bd4-4729-8289-5a25b22ff5b2"]}} +{"at": 1528, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_01GoyahVsaJSkqYZzb3ci9KQ", "name": "Bash", "input": {}, "caller": {"type": "direct"}}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "9e8e16c5-d002-4034-b955-3a8a00523ba4"}} +{"at": 1528, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "b21cf048-3dbe-4d71-9bb0-9c139be8fb12"}} +{"at": 2612, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"comma"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "2aa7968c-7244-47f1-8e0c-e660514037ee"}} +{"at": 2613, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "nd\": \"s"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "588330dc-afca-4aa7-b90c-73df15fee7c0"}} +{"at": 2613, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "leep 8\""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "75c0ecc9-4a50-4d6f-9731-3fed7e81d265"}} +{"at": 2613, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ", \"desc"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "a773fa58-791c-414f-94f1-a4a04534bdd7"}} +{"at": 2613, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ription"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "9de26ee7-6320-467a-b226-83ec143378d7"}} +{"at": 2613, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "\": \"Sle"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "e3871642-e9b2-4e35-890e-d8d3a7a81e1e"}} +{"at": 2613, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ep 8 se"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "7c1891c7-d7b2-48df-a26e-a7265389c8c7"}} +{"at": 2613, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "conds\"}"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "9d9be75c-c078-4bda-8a3f-dea3967d5a06"}} +{"at": 2615, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsCqF1QKvUNMHjZNCWW", "type": "message", "role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01GoyahVsaJSkqYZzb3ci9KQ", "name": "Bash", "input": {"command": "sleep 8", "description": "Sleep 8 seconds"}, "caller": {"type": "direct"}}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 16754, "cache_read_input_tokens": 0, "cache_creation": {"ephemeral_5m_input_tokens": 16754, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "0788da30-2441-4b32-803e-9ff172725739", "timestamp": "2026-09-28T12:50:26.842Z", "request_id": "req_011CfVsCppDCm7ZivTRsAYEu", "user_message_uuid": "c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "user_message_uuids": ["c1a5941b-1bd4-4729-8289-5a25b22ff5b2"]}} +{"at": 2619, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "40dac8c9-408f-4079-8d63-57e0f3b43ca1"}} +{"at": 2620, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "tool_use", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 16754, "cache_read_input_tokens": 0, "output_tokens": 81, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "6d1c0ce1-f7ad-463a-ab3d-f7745bce63f9"}} +{"at": 2621, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "b79333a8-7abc-4a32-a31f-249b5fea5f10"}} +{"at": 3640, "kind": "dispatch", "clientMessageId": "client-B", "sentUuid": "7ee6ab25-eafb-4869-85fd-97d058045906", "text": "Also say the word banana at the end of your reply.", "outcome": {"state": "admitted"}} +{"at": 3640, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "7ee6ab25-eafb-4869-85fd-97d058045906", "state": "queued", "uuid": "6395adb0-b318-4284-a298-7b3d89d6b7d7", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 5732, "kind": "frame", "frame": {"type": "system", "subtype": "task_started", "task_id": "beu0xkswp", "tool_use_id": "toolu_01GoyahVsaJSkqYZzb3ci9KQ", "description": "Sleep 8 seconds", "is_backgrounded": false, "task_type": "local_bash", "uuid": "a35b1b34-5810-47dd-bc2c-a38d19077a74", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 10740, "kind": "frame", "frame": {"type": "system", "subtype": "task_notification", "task_id": "beu0xkswp", "tool_use_id": "toolu_01GoyahVsaJSkqYZzb3ci9KQ", "status": "completed", "output_file": "", "summary": "Sleep 8 seconds", "uuid": "3f757c35-2f6c-4b21-bdf4-f8b523809abb", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 10743, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"tool_use_id": "toolu_01GoyahVsaJSkqYZzb3ci9KQ", "type": "tool_result", "content": "(Bash completed with no output)", "is_error": false}]}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "abbb0dbf-e102-4c66-ba3c-7808d634f675", "timestamp": "2026-09-28T12:50:34.971Z", "tool_use_result": {"stdout": "", "stderr": "", "interrupted": false, "isImage": false, "noOutputExpected": false}}} +{"at": 10744, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "Also say the word banana at the end of your reply."}]}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "7ee6ab25-eafb-4869-85fd-97d058045906", "timestamp": "2026-09-28T12:50:27.869Z", "isReplay": true}} +{"at": 10745, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "7ee6ab25-eafb-4869-85fd-97d058045906", "state": "started", "uuid": "30f94896-494d-4002-a1ce-680620e03d54", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 10745, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "bf42e496-1f7f-4095-8d94-19390bd59e50"}} +{"at": 11584, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsDaaVReUJ6r3tdEQhr", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 216, "cache_read_input_tokens": 16754, "cache_creation": {"ephemeral_5m_input_tokens": 216, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "79159e21-d876-467f-abac-86bfe7bf2457", "ttft_ms": 836}} +{"at": 11584, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_01MxJ1cYsSb1chARZ735hbRk", "name": "Bash", "input": {}, "caller": {"type": "direct"}}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "469a075b-ff35-42c8-b20e-fe4fd3ccf250"}} +{"at": 11585, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "1a0aeaf4-6b9e-441e-86ac-eac3c32204c4"}} +{"at": 12071, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"comma"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "6bb65599-1e9a-40d6-a769-e2b119fee09a"}} +{"at": 12072, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "nd\": \"s"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "04387767-2ce4-42ec-b869-11406432effc"}} +{"at": 12072, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "leep 8\""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ccc9d7c3-19a3-4691-a104-ae6b9c1b4dfa"}} +{"at": 12072, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ", \"desc"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "90f5078e-ff14-45a6-9377-257c4545bb5f"}} +{"at": 12072, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ription"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "8f04f554-0875-450a-bc0b-89cb339c864b"}} +{"at": 12072, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "\": \"Sle"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "6610a44a-019f-4136-a214-e0fc8e06d67f"}} +{"at": 12073, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ep 8 se"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "5caedf82-9c9e-4e13-ac26-d869ce84e863"}} +{"at": 12073, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "conds\"}"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "8151a90d-7bae-4805-b709-124001edee1d"}} +{"at": 12073, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsDaaVReUJ6r3tdEQhr", "type": "message", "role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01MxJ1cYsSb1chARZ735hbRk", "name": "Bash", "input": {"command": "sleep 8", "description": "Sleep 8 seconds"}, "caller": {"type": "direct"}}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 216, "cache_read_input_tokens": 16754, "cache_creation": {"ephemeral_5m_input_tokens": 216, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "6aaa7086-2892-4c73-a6b6-caef6cf1a2f5", "timestamp": "2026-09-28T12:50:36.299Z", "request_id": "req_011CfVsDa8gykAqKH3jsZE4P"}} +{"at": 12074, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "3a4aa6b1-647e-4228-94c5-decf6d567a2f"}} +{"at": 12074, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "tool_use", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 216, "cache_read_input_tokens": 16754, "output_tokens": 81, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "a09a8560-21b5-410c-9337-1abfb30caff1"}} +{"at": 12074, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "90522211-133a-4a50-8ae9-21bdef5590d1"}} +{"at": 15093, "kind": "frame", "frame": {"type": "system", "subtype": "task_started", "task_id": "btowzyjp1", "tool_use_id": "toolu_01MxJ1cYsSb1chARZ735hbRk", "description": "Sleep 8 seconds", "is_backgrounded": false, "task_type": "local_bash", "uuid": "c5aa424b-840a-43a2-9586-94feff2f1b97", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 20107, "kind": "frame", "frame": {"type": "system", "subtype": "task_notification", "task_id": "btowzyjp1", "tool_use_id": "toolu_01MxJ1cYsSb1chARZ735hbRk", "status": "completed", "output_file": "", "summary": "Sleep 8 seconds", "uuid": "9ad2b377-9339-4fe9-a2aa-ee9acdb41d08", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 20109, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"tool_use_id": "toolu_01MxJ1cYsSb1chARZ735hbRk", "type": "tool_result", "content": "(Bash completed with no output)", "is_error": false}]}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "e3b1fa1e-6a3d-4156-a1fe-2146aeff2b1e", "timestamp": "2026-09-28T12:50:44.337Z", "tool_use_result": {"stdout": "", "stderr": "", "interrupted": false, "isImage": false, "noOutputExpected": false}}} +{"at": 20110, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "990bcabe-2776-4b87-9ba9-788d3fff81cf"}} +{"at": 21105, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsEGxMrQeN7Y4miT6gp", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 120, "cache_read_input_tokens": 16970, "cache_creation": {"ephemeral_5m_input_tokens": 120, "ephemeral_1h_input_tokens": 0}, "output_tokens": 11, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "2d867ea9-894a-4dfa-a589-7a689402c6b2", "ttft_ms": 992}} +{"at": 21105, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ed2beb08-68a1-4159-953c-7a0b85729f4d"}} +{"at": 21106, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "FIRST DONE ban"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "581dc001-4ef8-4f1b-a741-0534f316d810"}} +{"at": 21106, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "ana"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "99a38535-8448-4953-b98e-f0740fc8e13a"}} +{"at": 21117, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsEGxMrQeN7Y4miT6gp", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "FIRST DONE banana"}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 120, "cache_read_input_tokens": 16970, "cache_creation": {"ephemeral_5m_input_tokens": 120, "ephemeral_1h_input_tokens": 0}, "output_tokens": 11, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "64e7945b-f3a9-409b-a58d-923a9c4a1f6a", "timestamp": "2026-09-28T12:50:45.334Z", "request_id": "req_011CfVsEGYJNniXhLXGpV5vr"}} +{"at": 21117, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "68da798f-ddd8-4af5-b012-ce4a10811bda"}} +{"at": 21117, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 120, "cache_read_input_tokens": 16970, "output_tokens": 11, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "29e16ff5-ab3b-4406-843f-049ee48650cd"}} +{"at": 21117, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "5b7d6bae-6827-48c9-8fab-e43e55949198"}} +{"at": 21119, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "7ee6ab25-eafb-4869-85fd-97d058045906", "state": "completed", "uuid": "0e958675-6553-46b7-9d98-5b667248666d", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 21120, "kind": "frame", "frame": {"duration_api_ms": 6661, "stop_reason": "end_turn", "session_id": "00000000-0000-4000-8000-00000000c1a0", "total_cost_usd": 0.10095080000000002, "usage": {"input_tokens": 6, "cache_creation_input_tokens": 17090, "cache_read_input_tokens": 33724, "output_tokens": 173, "output_tokens_details": {"thinking_tokens": 0}, "server_tool_use": {"web_search_requests": 0, "web_fetch_requests": 0}, "service_tier": "standard", "cache_creation": {"ephemeral_1h_input_tokens": 0, "ephemeral_5m_input_tokens": 17090}, "inference_geo": "not_available", "iterations": [], "speed": "standard"}, "modelUsage": {"claude-opus-5-5[1m]": {"inputTokens": 1219, "outputTokens": 194, "cacheReadInputTokens": 33724, "cacheCreationInputTokens": 17090, "webSearchRequests": 0, "costUSD": 0.10095080000000002, "contextWindow": 1000000, "maxOutputTokens": 128000, "thinkingTokens": 0, "canonicalModel": "claude-opus-5-5", "provider": "firstParty", "costBasis": "list"}}, "permission_denials": [], "terminal_reason": "completed", "fast_mode_state": "off", "fast_mode_disabled_reason": "sdk_opt_in_required", "subagent_stats": {"spawned": 0, "requested": {"background": 0, "foreground": 0, "unset": 0}, "started_in_background": 0, "max_depth": 0, "spawned_by_subagents": 0, "completed": 0, "failed": 0, "killed": {"parent": 0, "user": 0, "system": 0}, "refused": {"depth_limit": 0, "concurrency_limit": 0, "budget": 0}, "by_type": {}}, "is_error": false, "num_turns": 3, "subtype": "success", "api_error_status": null, "result": "FIRST DONE banana", "ttft_ms": 2340, "type": "result", "duration_ms": 20846, "uuid": "408f97b8-b880-47f8-b778-f94e4e15cd92", "ttft_stream_ms": 1253, "time_to_request_ms": 24, "first_content_frame_ms": 1253, "user_message_uuid": "c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "user_message_uuids": ["c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "7ee6ab25-eafb-4869-85fd-97d058045906"], "request_sent_wall_ms": 1790599824526, "queued_turn_count": 0, "result_index": 0}} +{"at": 21121, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "c1a5941b-1bd4-4729-8289-5a25b22ff5b2", "state": "completed", "uuid": "3b91d2ee-409f-45f5-a7be-aa7a45fb6ac5", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 21121, "kind": "frame", "frame": {"type": "system", "subtype": "session_state_changed", "state": "idle", "uuid": "4de40f8a-4b6c-4333-b1e6-ff528d249af9", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 24243, "kind": "end", "results": 1} diff --git a/src/main/claude/__fixtures__/claude-adapter-capture-miss.jsonl b/src/main/claude/__fixtures__/claude-adapter-capture-miss.jsonl new file mode 100644 index 00000000000..211f6e0eb8d --- /dev/null +++ b/src/main/claude/__fixtures__/claude-adapter-capture-miss.jsonl @@ -0,0 +1,85 @@ +{"at": 1, "kind": "meta", "scenario": "miss", "providerSessionId": "00000000-0000-4000-8000-00000000c1a0", "note": "Recorded through Orca adapter vs Claude CLI 2.1.280; scrubbed: session id, home/tmp paths."} +{"at": 228, "kind": "frame", "frame": {"type": "system", "subtype": "hook_started", "hook_id": "06af4bab-bef3-426b-96b7-733316a3b251", "hook_name": "SessionStart:startup", "hook_event": "SessionStart", "uuid": "9eb2ab97-dce5-430e-9db3-e7d9aa0e7f8d", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 239, "kind": "frame", "frame": {"type": "system", "subtype": "hook_response", "hook_id": "06af4bab-bef3-426b-96b7-733316a3b251", "hook_name": "SessionStart:startup", "hook_event": "SessionStart", "output": "\n", "stdout": "\n", "stderr": "", "exit_code": 0, "outcome": "success", "uuid": "e2266c8c-8e91-4893-9806-1286769a12fd", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 255, "kind": "dispatch", "clientMessageId": "client-A", "sentUuid": "712482eb-4c05-4f67-8888-a11843c2729f", "text": "Use the Bash tool to run `sleep 8` two separate times, one call at a time, waiting for each. Then reply exactly: FIRST DONE", "outcome": {"state": "admitted"}} +{"at": 257, "kind": "frame", "frame": {"type": "system", "subtype": "session_state_changed", "state": "running", "uuid": "a6e277fb-d7fc-418a-88f2-2d893feea0d0", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 257, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "712482eb-4c05-4f67-8888-a11843c2729f", "state": "queued", "uuid": "57d845c0-8945-4528-82a5-8fbe83381714", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 258, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "712482eb-4c05-4f67-8888-a11843c2729f", "state": "started", "uuid": "a2a8821a-7b4a-4332-95da-765eb95c2745", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 273, "kind": "frame", "frame": {"type": "system", "subtype": "init", "cwd": "/tmp/scrubbed-cwd", "session_id": "00000000-0000-4000-8000-00000000c1a0", "tools": ["Task", "AskUserQuestion", "Bash", "CronCreate", "CronDelete", "CronList", "DesignSync", "Edit", "EnterPlanMode", "EnterWorktree", "ExitPlanMode", "ExitWorktree", "ListAgents", "NotebookEdit", "Read", "ReportFindings", "ScheduleWakeup", "SendMessage", "Skill", "TaskCreate", "TaskGet", "TaskList", "TaskStop", "TaskUpdate", "ToolSearch", "WebFetch", "WebSearch", "Workflow", "Write"], "mcp_servers": [], "model": "claude-opus-5-5[1m]", "permissionMode": "default", "slash_commands": ["deep-research", "design", "design-sync", "dataviz", "update-config", "verify", "debug", "code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop", "claude-api", "workflow-authoring", "run", "run-skill-generator", "agents", "auto-mode-setup", "autocompact", "clear", "color", "compact", "config", "output-style", "context", "effort", "fast", "heapdump", "init", "mcp", "model", "__remote-workflow", "workflow-launch-exec", "reload-plugins", "reload-skills", "rename", "security-review", "usage", "insights", "recap", "goal", "design-consent", "design-revoke", "list-agents", "team-onboarding"], "terminal_slash_commands": ["doctor", "color", "reload-plugins"], "apiKeySource": "none", "claude_code_version": "2.1.280", "output_style": "default", "agents": ["claude", "Explore", "general-purpose", "Plan", "statusline-setup"], "skills": ["deep-research", "design", "design-sync", "dataviz", "update-config", "verify", "debug", "code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop", "claude-api", "workflow-authoring", "run", "run-skill-generator"], "plugins": [{"name": "telemetry", "path": "builtin", "source": "telemetry@builtin"}], "capabilities": ["interrupt_receipt_v1", "interrupt_cancel_queued_v1", "msg_lifecycle_v1", "mcp_read_resource_v1", "mcp_tool_ui_meta_v1"], "analytics_disabled": false, "product_feedback_disabled": false, "uuid": "ea0c12f1-7e48-4001-a577-5e4abb2e9c5f", "memory_paths": {"auto": "/Users/user/scrubbed"}, "messaging_socket_path": "/tmp/scrubbed.sock", "fast_mode_state": "off", "fast_mode_disabled_reason": "sdk_opt_in_required"}} +{"at": 275, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "85ed497a-5e78-4ac6-aa9c-2ebe37063403"}} +{"at": 1303, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "Use the Bash tool to run `sleep 8` two separate times, one call at a time, waiting for each. Then reply exactly: FIRST DONE"}]}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "712482eb-4c05-4f67-8888-a11843c2729f", "timestamp": "2026-09-28T12:51:16.373Z", "isReplay": true}} +{"at": 1303, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsGeUUTEuaYMdT2HtPQ", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 5244, "cache_read_input_tokens": 11514, "cache_creation": {"ephemeral_5m_input_tokens": 5244, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "61853b4b-5722-4b7b-94d8-15304f575d2c", "ttft_ms": 1022, "user_message_uuid": "712482eb-4c05-4f67-8888-a11843c2729f", "user_message_uuids": ["712482eb-4c05-4f67-8888-a11843c2729f"]}} +{"at": 1304, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_016sNyp5R1LwYkaBUj9UKWYs", "name": "Bash", "input": {}, "caller": {"type": "direct"}}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "df0f12a2-35bb-424c-8f28-40e6fcc07960"}} +{"at": 1304, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "5bcd51ce-460b-4891-8978-332c44f66114"}} +{"at": 2559, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"comma"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "9744edc1-59cc-4c22-959a-8bfba7abc2ef"}} +{"at": 2559, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "nd\": \"s"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "4b7645f6-f050-4271-91f0-544df1ff0931"}} +{"at": 2559, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "leep 8\""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "11acbd0f-fc8e-4642-8897-8815951dd4ee"}} +{"at": 2637, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ", \"desc"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "4d140c8b-47f3-44e4-aade-212f63e2b40a"}} +{"at": 2637, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ription"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "a51d0b53-c118-4de5-a848-aa4bdf7d86c7"}} +{"at": 2649, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "\": \"Sle"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "a128c776-0a6a-4923-952f-3633f4b91239"}} +{"at": 2649, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ep for "}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ddce72e4-cbcc-45f1-966d-dd873f59d687"}} +{"at": 2650, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "8 secon"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "44392f6b-e3ac-4caa-8abe-ee1b99ba6783"}} +{"at": 2650, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ds\"}"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "a5bc170f-84ba-4ac7-b104-b36d95c69049"}} +{"at": 2652, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsGeUUTEuaYMdT2HtPQ", "type": "message", "role": "assistant", "content": [{"type": "tool_use", "id": "toolu_016sNyp5R1LwYkaBUj9UKWYs", "name": "Bash", "input": {"command": "sleep 8", "description": "Sleep for 8 seconds"}, "caller": {"type": "direct"}}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 5244, "cache_read_input_tokens": 11514, "cache_creation": {"ephemeral_5m_input_tokens": 5244, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "4a86f704-4cdc-4561-840d-dee6e913ebf9", "timestamp": "2026-09-28T12:51:18.753Z", "request_id": "req_011CfVsGe1gx1SqtHypyjG4P", "user_message_uuid": "712482eb-4c05-4f67-8888-a11843c2729f", "user_message_uuids": ["712482eb-4c05-4f67-8888-a11843c2729f"]}} +{"at": 2655, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "f79e14ed-a3e2-4ebf-88bb-a45ed5ee6a78"}} +{"at": 2657, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "tool_use", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 5244, "cache_read_input_tokens": 11514, "output_tokens": 82, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "6b586bec-0544-4cdb-9fc5-fadae3c7d6ed"}} +{"at": 2657, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "3c602652-b8a2-4b38-b54d-ecb1f99b1201"}} +{"at": 5768, "kind": "frame", "frame": {"type": "system", "subtype": "task_started", "task_id": "bnnik2zr1", "tool_use_id": "toolu_016sNyp5R1LwYkaBUj9UKWYs", "description": "Sleep for 8 seconds", "is_backgrounded": false, "task_type": "local_bash", "uuid": "5213fad9-0ff9-4f01-b934-7f7155ba396b", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 10782, "kind": "frame", "frame": {"type": "system", "subtype": "task_notification", "task_id": "bnnik2zr1", "tool_use_id": "toolu_016sNyp5R1LwYkaBUj9UKWYs", "status": "completed", "output_file": "", "summary": "Sleep for 8 seconds", "uuid": "f37669e5-c402-43ac-823f-1a7f9005a834", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 10785, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"tool_use_id": "toolu_016sNyp5R1LwYkaBUj9UKWYs", "type": "tool_result", "content": "(Bash completed with no output)", "is_error": false}]}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "12161028-12c4-470e-a82d-1f6f4ee64aba", "timestamp": "2026-09-28T12:51:26.887Z", "tool_use_result": {"stdout": "", "stderr": "", "interrupted": false, "isImage": false, "noOutputExpected": false}}} +{"at": 10787, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "e0454575-2f1d-47c0-b9f3-4ecadbaaa122"}} +{"at": 11596, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsHQJiVw1uJvCEPhMGa", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 121, "cache_read_input_tokens": 16758, "cache_creation": {"ephemeral_5m_input_tokens": 121, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "834585ab-8f03-423d-ae43-825a12b9f45e", "ttft_ms": 805}} +{"at": 11597, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_01BiKvSfSEAwNM1DwshGhsT6", "name": "Bash", "input": {}, "caller": {"type": "direct"}}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "273975d6-06bf-4eff-87aa-dca1a6d39b5c"}} +{"at": 11597, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "2c9121cf-a472-476e-9626-05d4577ff349"}} +{"at": 11950, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"comma"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "c2cb4769-ee86-4eda-9af7-f75a89493101"}} +{"at": 11950, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "nd\": \"s"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "8e70eae8-653c-40ad-8a12-f37068234009"}} +{"at": 11950, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "leep 8\""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "0135507b-0b6e-41fe-bd1c-17becb10430f"}} +{"at": 12026, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ", \"desc"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "94802121-d71b-4781-856c-f40f78498ebe"}} +{"at": 12026, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ription"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ec03999d-3810-405e-b36a-58d8d0f4e4ac"}} +{"at": 12026, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "\": \"Sle"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "2183a548-e1b7-48c5-a4d8-58ab6ae4acc9"}} +{"at": 12026, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ep for "}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "d8ffd37e-3d9d-46a4-b6e1-d52cb96a334d"}} +{"at": 12026, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "8 secon"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "2c51be8c-ef09-42eb-a07b-c9083d3e349c"}} +{"at": 12026, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "ds\"}"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "e4957303-3d3f-47b4-aaed-0ec6210e1e81"}} +{"at": 12027, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsHQJiVw1uJvCEPhMGa", "type": "message", "role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01BiKvSfSEAwNM1DwshGhsT6", "name": "Bash", "input": {"command": "sleep 8", "description": "Sleep for 8 seconds"}, "caller": {"type": "direct"}}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 121, "cache_read_input_tokens": 16758, "cache_creation": {"ephemeral_5m_input_tokens": 121, "ephemeral_1h_input_tokens": 0}, "output_tokens": 16, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "03e0a7fa-a83c-4688-9504-8b7fa5c9f6ff", "timestamp": "2026-09-28T12:51:28.129Z", "request_id": "req_011CfVsHPsQjwKL4424e8EfN"}} +{"at": 12028, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "56ad793f-dcd0-4f9d-9dcc-49c2054d18c7"}} +{"at": 12036, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "tool_use", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 121, "cache_read_input_tokens": 16758, "output_tokens": 82, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "98e98f29-79a1-4db9-9cd4-c4fe810eca7e"}} +{"at": 12036, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "3629fbc7-6420-45ae-88be-cd9b779e721a"}} +{"at": 15034, "kind": "frame", "frame": {"type": "system", "subtype": "task_started", "task_id": "buewgpxis", "tool_use_id": "toolu_01BiKvSfSEAwNM1DwshGhsT6", "description": "Sleep for 8 seconds", "is_backgrounded": false, "task_type": "local_bash", "uuid": "fa9ac561-8bf7-4035-9aa8-221e4cd91dfb", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 20045, "kind": "frame", "frame": {"type": "system", "subtype": "task_notification", "task_id": "buewgpxis", "tool_use_id": "toolu_01BiKvSfSEAwNM1DwshGhsT6", "status": "completed", "output_file": "", "summary": "Sleep for 8 seconds", "uuid": "230550ea-a7f7-4171-9080-901cfdd0d571", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 20047, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"tool_use_id": "toolu_01BiKvSfSEAwNM1DwshGhsT6", "type": "tool_result", "content": "(Bash completed with no output)", "is_error": false}]}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "68545b22-4c1b-44df-a0ec-5876d2c19aee", "timestamp": "2026-09-28T12:51:36.149Z", "tool_use_result": {"stdout": "", "stderr": "", "interrupted": false, "isImage": false, "noOutputExpected": false}}} +{"at": 20048, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "e14bc8d3-a5bf-4d52-b841-9b5632247863"}} +{"at": 20123, "kind": "dispatch", "clientMessageId": "client-B", "sentUuid": "6f0204a5-a21a-47a4-9517-034cc4467b52", "text": "Also say the word banana at the end of your reply.", "outcome": {"state": "admitted"}} +{"at": 20124, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52", "state": "queued", "uuid": "87c94a31-75e5-400a-9551-ca24c36b9be5", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 21070, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsJ6bNbeN2V5Wbst25o", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 17000, "cache_read_input_tokens": 0, "cache_creation": {"ephemeral_5m_input_tokens": 17000, "ephemeral_1h_input_tokens": 0}, "output_tokens": 9, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "02c04bc0-68be-4721-a24d-27e7fa4a93db", "ttft_ms": 1019}} +{"at": 21071, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "c90f97b4-2cd4-482d-af20-cd921f5915ef"}} +{"at": 21071, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "FIRST DONE"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "059a395d-c0a0-4a06-b483-240c3aece7ff"}} +{"at": 21082, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsJ6bNbeN2V5Wbst25o", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "FIRST DONE"}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 17000, "cache_read_input_tokens": 0, "cache_creation": {"ephemeral_5m_input_tokens": 17000, "ephemeral_1h_input_tokens": 0}, "output_tokens": 9, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "fc532a8b-2d4d-4cb9-92a5-ea1c509a8019", "timestamp": "2026-09-28T12:51:37.173Z", "request_id": "req_011CfVsJ5eaEpxJgCRB1U4zr"}} +{"at": 21082, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "81e56773-385a-4016-a0ec-ce01d277b9d1"}} +{"at": 21082, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 17000, "cache_read_input_tokens": 0, "output_tokens": 9, "output_tokens_details": {"thinking_tokens": 0}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ea179db4-6487-4447-927e-ae75d31097a2"}} +{"at": 21082, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "6de38f87-568d-4573-960c-a6143bc8ecda"}} +{"at": 21085, "kind": "frame", "frame": {"duration_api_ms": 6803, "stop_reason": "end_turn", "session_id": "00000000-0000-4000-8000-00000000c1a0", "total_cost_usd": 0.1262354, "usage": {"input_tokens": 6, "cache_creation_input_tokens": 22365, "cache_read_input_tokens": 28272, "output_tokens": 173, "output_tokens_details": {"thinking_tokens": 0}, "server_tool_use": {"web_search_requests": 0, "web_fetch_requests": 0}, "service_tier": "standard", "cache_creation": {"ephemeral_1h_input_tokens": 0, "ephemeral_5m_input_tokens": 22365}, "inference_geo": "not_available", "iterations": [], "speed": "standard"}, "modelUsage": {"claude-opus-5-5[1m]": {"inputTokens": 1219, "outputTokens": 194, "cacheReadInputTokens": 28272, "cacheCreationInputTokens": 22365, "webSearchRequests": 0, "costUSD": 0.1262354, "contextWindow": 1000000, "maxOutputTokens": 128000, "thinkingTokens": 0, "canonicalModel": "claude-opus-5-5", "provider": "firstParty", "costBasis": "list"}}, "permission_denials": [], "terminal_reason": "completed", "fast_mode_state": "off", "fast_mode_disabled_reason": "sdk_opt_in_required", "subagent_stats": {"spawned": 0, "requested": {"background": 0, "foreground": 0, "unset": 0}, "started_in_background": 0, "max_depth": 0, "spawned_by_subagents": 0, "completed": 0, "failed": 0, "killed": {"parent": 0, "user": 0, "system": 0}, "refused": {"depth_limit": 0, "concurrency_limit": 0, "budget": 0}, "by_type": {}}, "is_error": false, "num_turns": 3, "subtype": "success", "api_error_status": null, "result": "FIRST DONE", "ttft_ms": 2391, "type": "result", "duration_ms": 20825, "uuid": "b82a7983-d7ea-496d-9cb7-735f1ffb3cda", "ttft_stream_ms": 1043, "time_to_request_ms": 22, "first_content_frame_ms": 1044, "user_message_uuid": "712482eb-4c05-4f67-8888-a11843c2729f", "user_message_uuids": ["712482eb-4c05-4f67-8888-a11843c2729f"], "request_sent_wall_ms": 1790599876383, "queued_turn_count": 0, "result_index": 0}} +{"at": 21085, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "712482eb-4c05-4f67-8888-a11843c2729f", "state": "completed", "uuid": "acb703de-6b64-4496-b552-74ddb042d768", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 21085, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52", "state": "started", "uuid": "77edaf35-52e5-42f1-a84a-f86dc68c96a4", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 21088, "kind": "frame", "frame": {"type": "system", "subtype": "init", "cwd": "/tmp/scrubbed-cwd", "session_id": "00000000-0000-4000-8000-00000000c1a0", "tools": ["Task", "AskUserQuestion", "Bash", "CronCreate", "CronDelete", "CronList", "DesignSync", "Edit", "EnterPlanMode", "EnterWorktree", "ExitPlanMode", "ExitWorktree", "ListAgents", "NotebookEdit", "Read", "ReportFindings", "ScheduleWakeup", "SendMessage", "Skill", "TaskCreate", "TaskGet", "TaskList", "TaskStop", "TaskUpdate", "ToolSearch", "WebFetch", "WebSearch", "Workflow", "Write"], "mcp_servers": [], "model": "claude-opus-5-5[1m]", "permissionMode": "default", "slash_commands": ["deep-research", "design", "design-sync", "dataviz", "update-config", "verify", "debug", "code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop", "claude-api", "workflow-authoring", "run", "run-skill-generator", "agents", "auto-mode-setup", "autocompact", "clear", "color", "compact", "config", "output-style", "context", "effort", "fast", "heapdump", "init", "mcp", "model", "__remote-workflow", "workflow-launch-exec", "reload-plugins", "reload-skills", "rename", "security-review", "usage", "insights", "recap", "goal", "design-consent", "design-revoke", "list-agents", "team-onboarding"], "terminal_slash_commands": ["doctor", "color", "reload-plugins"], "apiKeySource": "none", "claude_code_version": "2.1.280", "output_style": "default", "agents": ["claude", "Explore", "general-purpose", "Plan", "statusline-setup"], "skills": ["deep-research", "design", "design-sync", "dataviz", "update-config", "verify", "debug", "code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop", "claude-api", "workflow-authoring", "run", "run-skill-generator"], "plugins": [{"name": "telemetry", "path": "builtin", "source": "telemetry@builtin"}], "capabilities": ["interrupt_receipt_v1", "interrupt_cancel_queued_v1", "msg_lifecycle_v1", "mcp_read_resource_v1", "mcp_tool_ui_meta_v1"], "analytics_disabled": false, "product_feedback_disabled": false, "uuid": "f6af6108-710e-4208-8c3c-79b2a9fc14e4", "memory_paths": {"auto": "/Users/user/scrubbed"}, "messaging_socket_path": "/tmp/scrubbed.sock", "fast_mode_state": "off", "fast_mode_disabled_reason": "sdk_opt_in_required"}} +{"at": 21088, "kind": "frame", "frame": {"type": "system", "subtype": "status", "status": "requesting", "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "7ff5bb60-79be-49cb-bd26-950fa9584ffe"}} +{"at": 22022, "kind": "frame", "frame": {"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "Also say the word banana at the end of your reply."}]}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52", "timestamp": "2026-09-28T12:51:37.189Z", "isReplay": true}} +{"at": 22022, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_start", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsJAVHoszB7NSfdEtk4", "type": "message", "role": "assistant", "content": [], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 51, "cache_read_input_tokens": 17000, "cache_creation": {"ephemeral_5m_input_tokens": 51, "ephemeral_1h_input_tokens": 0}, "output_tokens": 7, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "30e634ea-7725-4e57-bba9-5b6ae1f44c1b", "ttft_ms": 927, "user_message_uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52", "user_message_uuids": ["6f0204a5-a21a-47a4-9517-034cc4467b52"]}} +{"at": 22023, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "", "signature": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "8c52275c-f862-4c49-a1b5-4cb5dde934c8"}} +{"at": 22469, "kind": "frame", "frame": {"type": "system", "subtype": "thinking_tokens", "estimated_tokens": 50, "estimated_tokens_delta": 50, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "91410a95-652d-4e88-8661-19ef21e01a22", "user_message_uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52"}} +{"at": 22469, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "", "estimated_tokens": 50}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "ea150b67-dad7-4436-84c5-82d73a3ee3ac"}} +{"at": 23097, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "", "estimated_tokens": null}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "dcf3a66c-0505-4b70-985f-d5d431de68c5"}} +{"at": 23097, "kind": "frame", "frame": {"type": "system", "subtype": "thinking_tokens", "estimated_tokens": 192, "estimated_tokens_delta": 142, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "411efdb6-1b14-40d5-b126-6251292c2898", "user_message_uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52"}} +{"at": 23097, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "CAQS9wUKEAgSGAI4AUIIdGhpbmtpbmcSDB/yUaKTcw1uKvgE2xoM//KByDA4fixole7KIjDLAtdDHZ4gV5AjYQGHGc1vQ+EwvB7yHUpWkhn/fsoZ0XTaERvUz6ZoVYhDH7TV/3EqlAW89rxYwkeD/V2vxPnz1iDOQvnSOBk08h5h6cd7CUNMyyM5UU/VtJ84BJDllyBTZLw8RCS/tuodap4P8NmO7I6wK6ZQ8d0hTfpyHsr7J4UfnD9RXhy2x5YdTX11OI8sQ7hxj9HkzhEJpJwRSytLRpclM3ATKwIv/ApihNhfYtbKApQSeIOlFArXz17Xm3RYMs3WjRzL8NMPmV0d5sirOCJ1xtAf3vLSi7awlvG45tpmkW3xaEZsVczOBSNhLPdzwEuHQrAZqx+5WE/b5cC9A2jfW3HgOSdmlmp1z0vYhdbvfu+ALKLUFmAz49QY8YINKNXLbZMUxY5IEDHrRUNjQIY2tJvJWi1SMNNaAvRh1dVBwRDdCXRkWt1Yt8U3iQ2g0dZnfeEGCrIETtzGnRZ5nY0ekGcVrU8QQCCiibceC0dSOmbpNOnDGjxpu3PdoBBoxjZB/Ys1I2td+KDsGwWi0KygWOH7xoa11fsLHvoBu+d0RaTCH64uX6s3bLkajeeCpX0nEt0PR0GT69Jst6J4JwiDTDjjK4LF87Sxm4tSnTzymEYXhk2M5EtdkOUmD5yeI4Qf0liMutVlLWISHzDsAj/t1i7dQtr76wHzrq0/1qdf4OJlFH0qLEM98ekpLOsESLLGryp0UIRIrMaJtpk8e/o9+uHgCP2IwxxEFaWb6PUvNvuI00K/C3dxTD9JQQrNZRdadsnj74/TR2qePDAXzb8vhDSm2b8umySGmHCPrTjBdC0O6riijl1pKqdjRS9yzFVInnZ00zUHpmT9ojVT3dKennDIfdN4ShxGTh5be81hF1pfm9GDC7HJGpG+Eth3qS8JTRQhDx43J4DKNA4XQYQ7CHIxOLET/s8e+qGLnlGCJl6K0h4YAQ=="}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "3b70c219-37e8-485d-a00c-48e3680cc14e"}} +{"at": 23097, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsJAVHoszB7NSfdEtk4", "type": "message", "role": "assistant", "content": [{"type": "thinking", "thinking": "", "signature": "CAQS9wUKEAgSGAI4AUIIdGhpbmtpbmcSDB/yUaKTcw1uKvgE2xoM//KByDA4fixole7KIjDLAtdDHZ4gV5AjYQGHGc1vQ+EwvB7yHUpWkhn/fsoZ0XTaERvUz6ZoVYhDH7TV/3EqlAW89rxYwkeD/V2vxPnz1iDOQvnSOBk08h5h6cd7CUNMyyM5UU/VtJ84BJDllyBTZLw8RCS/tuodap4P8NmO7I6wK6ZQ8d0hTfpyHsr7J4UfnD9RXhy2x5YdTX11OI8sQ7hxj9HkzhEJpJwRSytLRpclM3ATKwIv/ApihNhfYtbKApQSeIOlFArXz17Xm3RYMs3WjRzL8NMPmV0d5sirOCJ1xtAf3vLSi7awlvG45tpmkW3xaEZsVczOBSNhLPdzwEuHQrAZqx+5WE/b5cC9A2jfW3HgOSdmlmp1z0vYhdbvfu+ALKLUFmAz49QY8YINKNXLbZMUxY5IEDHrRUNjQIY2tJvJWi1SMNNaAvRh1dVBwRDdCXRkWt1Yt8U3iQ2g0dZnfeEGCrIETtzGnRZ5nY0ekGcVrU8QQCCiibceC0dSOmbpNOnDGjxpu3PdoBBoxjZB/Ys1I2td+KDsGwWi0KygWOH7xoa11fsLHvoBu+d0RaTCH64uX6s3bLkajeeCpX0nEt0PR0GT69Jst6J4JwiDTDjjK4LF87Sxm4tSnTzymEYXhk2M5EtdkOUmD5yeI4Qf0liMutVlLWISHzDsAj/t1i7dQtr76wHzrq0/1qdf4OJlFH0qLEM98ekpLOsESLLGryp0UIRIrMaJtpk8e/o9+uHgCP2IwxxEFaWb6PUvNvuI00K/C3dxTD9JQQrNZRdadsnj74/TR2qePDAXzb8vhDSm2b8umySGmHCPrTjBdC0O6riijl1pKqdjRS9yzFVInnZ00zUHpmT9ojVT3dKennDIfdN4ShxGTh5be81hF1pfm9GDC7HJGpG+Eth3qS8JTRQhDx43J4DKNA4XQYQ7CHIxOLET/s8e+qGLnlGCJl6K0h4YAQ=="}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 51, "cache_read_input_tokens": 17000, "cache_creation": {"ephemeral_5m_input_tokens": 51, "ephemeral_1h_input_tokens": 0}, "output_tokens": 7, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "f59a0f60-bc42-4d2f-be93-2d89991bf482", "timestamp": "2026-09-28T12:51:39.200Z", "request_id": "req_011CfVsJA4Uy2C2Rx8uyY7H1", "user_message_uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52", "user_message_uuids": ["6f0204a5-a21a-47a4-9517-034cc4467b52"]}} +{"at": 23097, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 0}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "0d8eff25-307b-4247-81e1-9a4575c7e07d"}} +{"at": 23097, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "7328034f-41e4-4823-9bde-8df483de1ce5"}} +{"at": 23098, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "FIRST DONE ban"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "e8f3de55-f311-4eac-baab-9db3fe5ec197"}} +{"at": 23098, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "ana"}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "b293546a-d0c5-4cdd-9884-5b155b845e96"}} +{"at": 23098, "kind": "frame", "frame": {"type": "assistant", "message": {"model": "claude-opus-5-5", "id": "msg_011CfVsJAVHoszB7NSfdEtk4", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "FIRST DONE banana"}], "container": null, "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 51, "cache_read_input_tokens": 17000, "cache_creation": {"ephemeral_5m_input_tokens": 51, "ephemeral_1h_input_tokens": 0}, "output_tokens": 7, "service_tier": "standard", "inference_geo": "not_available"}, "diagnostics": null, "context_management": null}, "parent_tool_use_id": null, "session_id": "00000000-0000-4000-8000-00000000c1a0", "uuid": "31583f02-16b9-418d-89b3-269afb1dc2c0", "timestamp": "2026-09-28T12:51:39.201Z", "request_id": "req_011CfVsJA4Uy2C2Rx8uyY7H1"}} +{"at": 23098, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "content_block_stop", "index": 1}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "f8047b07-9ca2-436f-a8e2-f05a17f14970"}} +{"at": 23105, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null, "stop_details": null, "container": null}, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 51, "cache_read_input_tokens": 17000, "output_tokens": 65, "output_tokens_details": {"thinking_tokens": 54}}, "context_management": {"applied_edits": []}}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "372ebe0e-8f36-4567-8563-4aff3e059a3f"}} +{"at": 23105, "kind": "frame", "frame": {"type": "stream_event", "event": {"type": "message_stop"}, "session_id": "00000000-0000-4000-8000-00000000c1a0", "parent_tool_use_id": null, "uuid": "b11714ab-fe3d-44fb-991d-c62ae8a60507"}} +{"at": 23107, "kind": "frame", "frame": {"duration_api_ms": 8819, "stop_reason": "end_turn", "session_id": "00000000-0000-4000-8000-00000000c1a0", "total_cost_usd": 0.1311984, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 51, "cache_read_input_tokens": 17000, "output_tokens": 65, "output_tokens_details": {"thinking_tokens": 54}, "server_tool_use": {"web_search_requests": 0, "web_fetch_requests": 0}, "service_tier": "standard", "cache_creation": {"ephemeral_1h_input_tokens": 0, "ephemeral_5m_input_tokens": 51}, "inference_geo": "not_available", "iterations": [], "speed": "standard"}, "modelUsage": {"claude-opus-5-5[1m]": {"inputTokens": 1221, "outputTokens": 259, "cacheReadInputTokens": 45272, "cacheCreationInputTokens": 22416, "webSearchRequests": 0, "costUSD": 0.1311984, "contextWindow": 1000000, "maxOutputTokens": 128000, "thinkingTokens": 54, "canonicalModel": "claude-opus-5-5", "provider": "firstParty", "costBasis": "list"}}, "permission_denials": [], "terminal_reason": "completed", "fast_mode_state": "off", "fast_mode_disabled_reason": "sdk_opt_in_required", "subagent_stats": {"spawned": 0, "requested": {"background": 0, "foreground": 0, "unset": 0}, "started_in_background": 0, "max_depth": 0, "spawned_by_subagents": 0, "completed": 0, "failed": 0, "killed": {"parent": 0, "user": 0, "system": 0}, "refused": {"depth_limit": 0, "concurrency_limit": 0, "budget": 0}, "by_type": {}}, "is_error": false, "num_turns": 1, "subtype": "success", "api_error_status": null, "result": "FIRST DONE banana", "ttft_ms": 2012, "type": "result", "duration_ms": 2021, "uuid": "572df995-0b87-436c-9966-5f3740c22820", "ttft_stream_ms": 937, "time_to_request_ms": 10, "first_content_frame_ms": 937, "user_message_uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52", "user_message_uuids": ["6f0204a5-a21a-47a4-9517-034cc4467b52"], "request_sent_wall_ms": 1790599897198, "queued_turn_count": 0, "result_index": 1}} +{"at": 23107, "kind": "frame", "frame": {"type": "command_lifecycle", "command_uuid": "6f0204a5-a21a-47a4-9517-034cc4467b52", "state": "completed", "uuid": "bd6b5dbd-002f-4d5e-8311-7d747745ea00", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 23108, "kind": "frame", "frame": {"type": "system", "subtype": "session_state_changed", "state": "idle", "uuid": "c7642956-8764-404f-8b89-8eac3a91cb1e", "session_id": "00000000-0000-4000-8000-00000000c1a0"}} +{"at": 26162, "kind": "end", "results": 2} diff --git a/src/main/claude/claude-adapter-capture-replay.test.ts b/src/main/claude/claude-adapter-capture-replay.test.ts new file mode 100644 index 00000000000..8e877a135a8 --- /dev/null +++ b/src/main/claude/claude-adapter-capture-replay.test.ts @@ -0,0 +1,233 @@ +// Verbatim replay of FULL captures recorded through Orca's own adapter against +// the real CLI (`__fixtures__/claude-adapter-capture-*.jsonl`): every frame the +// adapter saw — hook proof, per-cycle init, command_lifecycle, session-state, +// stream events — in the recorded order, with the recorded dispatch points. +// The oracle is the provider's own membership fact: each result's +// `user_message_uuids` must equal the sends that settled into the turn that +// result closed. Nothing here asserts design internals, so a rule change or a +// CLI drift that breaks membership fails these tests whatever the mechanism. + +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it, vi, type Mock } from 'vitest' +import type { + AgentJournalItemBody, + AgentJournalMessageItem +} from '../../shared/agent-session-journal-types' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { readAgentJournalTurn } from '../../shared/agent-session-turn-record' +import { ClaudeStructuredSessionAdapter } from './claude-structured-session-adapter' +import type { + ClaudeLateDispatchOutcome, + ClaudeStructuredSessionAdapterDeps +} from './claude-structured-session-state' +import { + fakeClaude, + identityFor, + PROVIDER_SESSION_ID +} from './claude-structured-session-test-support' + +type CapturedEvent = + | { at: number; kind: 'meta'; providerSessionId: string } + | { at: number; kind: 'frame'; frame: Record } + | { at: number; kind: 'dispatch'; clientMessageId: string; sentUuid: string; text: string } + | { at: number; kind: 'end' } + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function decodeCapturedEvent(value: unknown): CapturedEvent { + if (!isRecord(value) || typeof value.at !== 'number' || typeof value.kind !== 'string') { + throw new Error('capture line is not a recorded event') + } + if (value.kind === 'meta' && typeof value.providerSessionId === 'string') { + return { at: value.at, kind: 'meta', providerSessionId: value.providerSessionId } + } + if (value.kind === 'frame' && isRecord(value.frame)) { + return { at: value.at, kind: 'frame', frame: value.frame } + } + if ( + value.kind === 'dispatch' && + typeof value.clientMessageId === 'string' && + typeof value.sentUuid === 'string' && + typeof value.text === 'string' + ) { + return { + at: value.at, + kind: 'dispatch', + clientMessageId: value.clientMessageId, + sentUuid: value.sentUuid, + text: value.text + } + } + if (value.kind === 'end') { + return { at: value.at, kind: 'end' } + } + throw new Error(`capture line has unknown kind: ${String(value.kind)}`) +} + +function loadCapture(name: string): CapturedEvent[] { + const path = join(__dirname, '__fixtures__', `claude-adapter-capture-${name}.jsonl`) + return readFileSync(path, 'utf8') + .trim() + .split('\n') + .map((line) => decodeCapturedEvent(JSON.parse(line))) +} + +type Replay = { + settled: Mock + /** Final revision per turn id, in first-open order. */ + finalTurns: Map>> + everInterrupted: boolean + /** Each result frame's `user_message_uuids`, mapped to live dispatch uuids. */ + resultMemberships: string[][] + /** clientMessageId -> provider uuid its delivery settled under. */ + settledUuids: Map + /** Live dispatch uuid -> turn id its send landed in (turn open at settlement). */ + liveUuidByClient: Map +} + +async function replayCapture(name: string): Promise { + const capture = loadCapture(name) + let nowMs = 1_700_000_200_000 + const finalTurns = new Map>>() + let everInterrupted = false + const sink: StructuredAgentSessionEventSink = { + appendItem: (_identity, body: AgentJournalItemBody) => { + const turn = readAgentJournalTurn(body) + if (turn) { + finalTurns.set(turn.turnId, turn) + everInterrupted ||= turn.state === 'interrupted' + } + }, + appendTombstone: () => {}, + publish: () => {} + } + const settled = vi.fn<(input: { sessionId: string } & ClaudeLateDispatchOutcome) => void>() + // The capture supplies every frame, startup proof included. + const claude = fakeClaude({ initProof: 'none', replayUuid: null }) + const deps: ClaudeStructuredSessionAdapterDeps = { + resolveLaunch: async () => ({ + pathToClaudeCodeExecutable: 'claude', + options: {}, + cwd: '/work/repo', + claudeConfigDir: '/accounts/claude', + providerSessionId: PROVIDER_SESSION_ID, + resumeLeafUuid: null, + resumesTranscript: false, + continuesChain: false + }), + openConnection: claude.openConnection, + readProcessStartTime: async () => 1, + now: () => nowMs, + persistHandle: async () => {}, + onDispatchSettledLate: settled + } + const adapter = new ClaudeStructuredSessionAdapter(deps) + await adapter.acquire({ identity: identityFor(), fence: 7, spawnToken: 'spawn-9', events: sink }) + + // Captured uuids -> the uuids the live dispatches mint during this replay. + const uuidMap = new Map() + const capturedSessionId = capture.flatMap((event) => + event.kind === 'meta' ? [event.providerSessionId] : [] + )[0] + const mapFrame = (frame: Record): Record => { + let text = JSON.stringify(frame) + for (const [captured, live] of uuidMap) { + text = text.replaceAll(captured, live) + } + if (capturedSessionId) { + text = text.replaceAll(capturedSessionId, PROVIDER_SESSION_ID) + } + const mapped: unknown = JSON.parse(text) + if (!isRecord(mapped)) { + throw new Error('mapped frame is not a record') + } + return mapped + } + const resultMemberships: string[][] = [] + let startedAwaited = false + for (const event of capture) { + nowMs = 1_700_000_200_000 + event.at + if (event.kind === 'dispatch') { + if (!startedAwaited) { + // The proof frames have been delivered by now; startup can settle. + await adapter.awaitStarted('session-1') + startedAwaited = true + } + const body: AgentJournalMessageItem = { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: event.text }] + } + const before = new Set(uuidMap.values()) + const outcome = await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: event.clientMessageId, + body, + requestedAt: nowMs, + fence: 7 + }) + expect(outcome).toEqual({ state: 'admitted' }) + const sent = claude.connections[0]!.sent.at(-1)?.uuid + if (typeof sent !== 'string' || before.has(sent)) { + throw new Error('replay could not read the dispatched uuid') + } + uuidMap.set(event.sentUuid, sent) + } else if (event.kind === 'frame') { + const frame = mapFrame(event.frame) + if (frame.type === 'result') { + const members = Array.isArray(frame.user_message_uuids) ? frame.user_message_uuids : [] + resultMemberships.push(members.filter((m): m is string => typeof m === 'string')) + } + claude.connections[0]!.handlers.onMessage?.(frame) + } + } + const settledUuids = new Map() + for (const [outcome] of settled.mock.calls) { + if ('providerIdentity' in outcome && outcome.providerIdentity.provider === 'claude') { + settledUuids.set(outcome.clientMessageId, outcome.providerIdentity.uuid) + } + } + const liveUuidByClient = new Map() + for (const event of capture) { + if (event.kind === 'dispatch') { + liveUuidByClient.set(event.clientMessageId, uuidMap.get(event.sentUuid)!) + } + } + return { settled, finalTurns, everInterrupted, resultMemberships, settledUuids, liveUuidByClient } +} + +describe.each(['fold', 'early-steer', 'miss'])('adapter capture replay: %s', (name) => { + it('keeps turn membership equal to each result’s user_message_uuids', async () => { + const replay = await replayCapture(name) + + // One turn per result, each opened by that result's first member, and no + // turn ever marked interrupted (no capture interrupts one). + expect([...replay.finalTurns.keys()]).toEqual( + replay.resultMemberships.map((members) => members[0]) + ) + expect(replay.everInterrupted).toBe(false) + for (const turn of replay.finalTurns.values()) { + expect(turn.state).toBe('completed') + } + + // Every send settled accepted under a provider uuid named by exactly the + // result whose turn it belongs to — the provider's membership fact. + const allMembers = new Set(replay.resultMemberships.flat()) + for (const [clientMessageId, liveUuid] of replay.liveUuidByClient) { + expect(replay.settledUuids.get(clientMessageId)).toBe(liveUuid) + expect(allMembers.has(liveUuid)).toBe(true) + } + for (const [index, members] of replay.resultMemberships.entries()) { + for (const member of members) { + // A member of result i must not have opened any OTHER turn. + const owner = [...replay.finalTurns.keys()].indexOf(member) + if (owner !== -1) { + expect(owner).toBe(index) + } + } + } + }) +}) diff --git a/src/main/claude/claude-agent-sdk-process-spawn.test.ts b/src/main/claude/claude-agent-sdk-process-spawn.test.ts index cd3520cf6d5..1b6c192d433 100644 --- a/src/main/claude/claude-agent-sdk-process-spawn.test.ts +++ b/src/main/claude/claude-agent-sdk-process-spawn.test.ts @@ -4,14 +4,22 @@ import { describe, expect, it, vi } from 'vitest' import type { SpawnOptions as SdkSpawnOptions } from '@anthropic-ai/claude-agent-sdk' import { resolveSpawn, type spawnProcess } from '../../shared/child-process/run-process' import type { ProcessSpec } from '../../shared/child-process/process-spec' +import type * as ProviderSupervisor from '../codex/codex-app-server-posix-supervisor' +import { createProviderSpawnSpec } from '../codex/codex-app-server-posix-supervisor' import { createClaudeCodeProcessSpawn } from './claude-agent-sdk-process-spawn' +import { proveClaudeChildExitWithReaper } from './claude-child-exit-proof-ladder' + +vi.mock('../codex/codex-app-server-posix-supervisor', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, createProviderSpawnSpec: vi.fn(actual.createProviderSpawnSpec) } +}) type FakeChild = EventEmitter & { pid: number stdin: PassThrough stdout: PassThrough stderr: PassThrough - kill: ReturnType + kill: ReturnType boolean>> } function fakeSpawn() { @@ -20,7 +28,7 @@ function fakeSpawn() { child.stdin = new PassThrough() child.stdout = new PassThrough() child.stderr = new PassThrough() - child.kill = vi.fn(() => true) + child.kill = vi.fn((_signal?: NodeJS.Signals | number) => true) const specs: ProcessSpec[] = [] const spawnImpl = ((spec: ProcessSpec) => { specs.push(spec) @@ -43,7 +51,7 @@ function sdkOptions(overrides: Partial = {}): SdkSpawnOptions { describe('claude agent SDK process spawn', () => { it('routes the SDK spawn through Orca and retains the pid the lease adjudicates on', () => { const process = fakeSpawn() - const spawn = createClaudeCodeProcessSpawn(process.spawnImpl) + const spawn = createClaudeCodeProcessSpawn(process.spawnImpl, 'win32') expect(spawn.pid).toBeUndefined() expect(spawn.child).toBeNull() @@ -52,15 +60,103 @@ describe('claude agent SDK process spawn', () => { expect(child).toBe(process.child) expect(spawn.child).toBe(process.child) expect(spawn.pid).toBe(4321) + // Windows has no supervisor: Claude itself is the child. + expect(spawn.supervised).toBe(false) expect(process.specs[0]).toEqual({ program: '/usr/local/bin/claude', args: ['--output-format', 'stream-json'], cwd: '/work/repo', env: { PATH: '/usr/bin', CLAUDE_CONFIG_DIR: '/accounts/one' }, + detached: false, stdio: ['pipe', 'pipe', 'pipe'] }) }) + it.each(['darwin', 'linux'] as const)( + 'starts Claude under the provider supervisor on %s, which is then the pid the lease records', + (platform) => { + const process = fakeSpawn() + const spawn = createClaudeCodeProcessSpawn(process.spawnImpl, platform) + spawn.spawn(sdkOptions()) + + const [spec] = process.specs + if (!spec) { + throw new Error('the spawner never built a spec') + } + expect(spawn.supervised).toBe(true) + expect(spawn.pid).toBe(4321) + expect(spec.program).toBe(globalThis.process.execPath) + expect(spec.args?.[0]).toBe('-e') + expect(spec.detached).toBe(true) + expect(spec.cwd).toBe('/work/repo') + const supervisorSpec = JSON.parse( + Buffer.from(String(spec.env?.ORCA_PROVIDER_SUPERVISOR_SPEC), 'base64').toString() + ) + expect(supervisorSpec).toMatchObject({ + command: '/usr/local/bin/claude', + args: ['--output-format', 'stream-json'], + cwd: '/work/repo', + ownerPid: globalThis.process.pid + }) + // The supervisor passes its env to Claude minus its own two keys. + expect(spec.env).toMatchObject({ PATH: '/usr/bin', CLAUDE_CONFIG_DIR: '/accounts/one' }) + } + ) + + it.each([ + { platform: 'darwin', specSupervised: false }, + { platform: 'win32', specSupervised: true } + ] as const)( + 'stops Claude by the spawn spec\u2019s supervision on $platform, never the platform', + async ({ platform, specSupervised }) => { + const actual = await vi.importActual( + '../codex/codex-app-server-posix-supervisor' + ) + vi.mocked(createProviderSpawnSpec).mockImplementationOnce((...args) => ({ + ...actual.createProviderSpawnSpec(...args), + supervised: specSupervised + })) + const process = fakeSpawn() + const spawn = createClaudeCodeProcessSpawn(process.spawnImpl, platform) + spawn.spawn(sdkOptions()) + expect(spawn.supervised).toBe(specSupervised) + + let exited = false + let settle = (): void => {} + const exitPromise = new Promise((resolve) => { + settle = resolve + }) + // Claude leaves shortly after stdin ends, so the ladder never needs its forced rung. + process.child.stdin.on('finish', () => + setTimeout(() => { + exited = true + settle() + }, 10) + ) + const tree = { + capture: vi.fn(async () => {}), + reap: vi.fn(async () => 'exited' as const), + treeVerdict: 'exited' as const + } + await proveClaudeChildExitWithReaper( + { + child: process.child, + exitPromise, + exited: () => exited, + tree, + supervised: spawn.supervised + }, + () => tree + ) + // SIGTERM to an unsupervised Claude on Windows is TerminateProcess; a skipped one leaves it running. + if (specSupervised) { + expect(process.child.kill).toHaveBeenCalledWith('SIGTERM') + } else { + expect(process.child.kill).not.toHaveBeenCalled() + } + } + ) + it('keeps the child out of the SDK abort path so exit proof stays Orca-owned', () => { const process = fakeSpawn() const controller = new AbortController() @@ -86,7 +182,7 @@ describe('claude agent SDK process spawn', () => { it('hands a Windows .cmd shim to Orca\u2019s argument encoder', () => { const process = fakeSpawn() - createClaudeCodeProcessSpawn(process.spawnImpl).spawn( + createClaudeCodeProcessSpawn(process.spawnImpl, 'win32').spawn( sdkOptions({ command: 'C:\\Users\\dev\\AppData\\npm\\claude.cmd', args: ['--setting-sources=user,project,local', '--session-id', 'a b&c'] diff --git a/src/main/claude/claude-agent-sdk-process-spawn.ts b/src/main/claude/claude-agent-sdk-process-spawn.ts index a2b1ad7f158..bbb4b295aad 100644 --- a/src/main/claude/claude-agent-sdk-process-spawn.ts +++ b/src/main/claude/claude-agent-sdk-process-spawn.ts @@ -1,5 +1,6 @@ import type { SpawnOptions as ClaudeAgentSdkSpawnOptions } from '@anthropic-ai/claude-agent-sdk' import { spawnProcess } from '../../shared/child-process/run-process' +import { createProviderSpawnSpec } from '../codex/codex-app-server-posix-supervisor' /** Derived rather than imported: only src/shared/child-process may name node:child_process. */ type ClaudeCodeChild = ReturnType @@ -11,8 +12,13 @@ export type ClaudeCodeProcessSpawn = { spawn: (options: ClaudeAgentSdkSpawnOptions) => ClaudeCodeChild /** The retained child, so Orca keeps its own tree-kill and exit-proof ladder. Null until the SDK spawns. */ readonly child: ClaudeCodeChild | null - /** Ownership proof: the durable lease adjudicates on this pid plus start time plus the spawn token. */ + /** + * Ownership proof: the durable lease adjudicates on this pid plus start time plus the spawn + * token. On POSIX it is the provider supervisor's, which outlives Claude by construction. + */ readonly pid: number | undefined + /** The spawn spec's verdict, so the close ladder never re-decides it. False until the SDK spawns. */ + readonly supervised: boolean readonly stderrTail: string } @@ -31,24 +37,41 @@ function definedEnv(env: Record): Record { + const spec = createProviderSpawnSpec( + { + command: options.command, + args: [...options.args], + ...(options.cwd === undefined ? {} : { cwd: options.cwd }) + }, + definedEnv(options.env), + platform + ) // Why `options.signal` is dropped: it would let the SDK kill the child outside // Orca's ladder, and close() may never report an exit it did not observe. const spawned = spawnImpl({ - program: options.command, - args: [...options.args], - ...(options.cwd === undefined ? {} : { cwd: options.cwd }), - env: definedEnv(options.env), + program: spec.program, + args: spec.args, + cwd: spec.cwd, + env: spec.env, + detached: spec.detached, stdio: ['pipe', 'pipe', 'pipe'] }) child = spawned + supervised = spec.supervised // The SDK drains stderr only for its own local spawn, so a custom spawner must: // otherwise the child blocks on a full pipe and exit errors lose their tail. spawned.stderr.setEncoding('utf8').on('data', (chunk: string) => { @@ -62,6 +85,9 @@ export function createClaudeCodeProcessSpawn( get pid() { return child?.pid }, + get supervised() { + return supervised + }, get stderrTail() { return stderrTail } diff --git a/src/main/claude/claude-can-use-tool-test-support.ts b/src/main/claude/claude-can-use-tool-test-support.ts new file mode 100644 index 00000000000..cbf064a0933 --- /dev/null +++ b/src/main/claude/claude-can-use-tool-test-support.ts @@ -0,0 +1,30 @@ +// Invoking the fake connection's canUseTool permission callback, as the CLI would. + +import type { ClaudeStreamJsonConnectionHandlers } from './claude-stream-json-connection' +import type { FakeConnection } from './claude-structured-session-test-support' + +export function invokeCanUseTool( + connection: FakeConnection, + toolName: string, + requestId: string, + toolUseID: string, + extra: { + input?: Record + suggestions?: unknown[] + signal?: AbortSignal + } = {} +): { promise: Promise; settled: () => boolean } { + const options = { + requestId, + toolUseID, + signal: extra.signal ?? new AbortController().signal, + ...(extra.suggestions ? { suggestions: extra.suggestions } : {}) + } as unknown as Parameters>[2] + let done = false + const promise = Promise.resolve( + connection.handlers.canUseTool?.(toolName, extra.input ?? {}, options) + ).finally(() => { + done = true + }) + return { promise, settled: () => done } +} diff --git a/src/main/claude/claude-captured-fold-steer-frames.test-fixture.ts b/src/main/claude/claude-captured-fold-steer-frames.test-fixture.ts new file mode 100644 index 00000000000..8213b4bfa45 --- /dev/null +++ b/src/main/claude/claude-captured-fold-steer-frames.test-fixture.ts @@ -0,0 +1,275 @@ +// Claude CLI 2.1.280 stream-json captures of sends made while a turn is running +// (`--replay-user-messages`), cut to the frames and fields the dispatch/turn path +// reads. Ids, paths and prompts are replaced; the frame order and the relative +// clock (`at`, ms after the first send) are the captured ones. +// +// The captured shape under test: a mid-turn send the CLI folds into the running +// turn is replayed as a root user frame (`isReplay: true`, the client uuid) while +// that turn is still open, and the turn's ONE result names every folded send in +// `user_message_uuids`. A send the CLI runs later (miss) is replayed only when its +// own turn starts, after the first result. A cancelled mid-turn send is never +// replayed at all. + +import { + assistant, + assistantText, + assistantToolUse, + initFrame, + resultFrame, + sessionIdle, + taskFrame, + toolResult, + userReplay, + type CapturedFoldFrame, + type FoldCaptureIds +} from './claude-fold-steer-frame-builders.test-fixture' + +export type { CapturedFoldFrame, FoldCaptureIds } + +export const FIRST_PROMPT = 'Run the sleep command three times, then reply: FIRST DONE' +export const STEER_PROMPT = 'Also say the word banana at the end of your reply.' +export const SECOND_STEER_PROMPT = 'And also say the word mango at the very end.' + +/** p2-fold-fresh: first send at 12, steer at 3878; one result names both sends. */ +export const FOLD_FRESH_SEND_AT = { first: 12, steer: 3_878 } +export function foldFreshCapture({ sessionId, first, steer }: FoldCaptureIds): CapturedFoldFrame[] { + return [ + initFrame(222, sessionId), + userReplay(2_094, sessionId, first, FIRST_PROMPT), + assistantToolUse(2_676, sessionId, 'reply-tool-1', 'toolu_sleep_1', [first]), + toolResult(7_890, sessionId, 'tool-result-1', 'toolu_sleep_1'), + userReplay(7_892, sessionId, steer, STEER_PROMPT), + assistantToolUse(9_725, sessionId, 'reply-tool-2', 'toolu_sleep_2'), + toolResult(14_740, sessionId, 'tool-result-2', 'toolu_sleep_2'), + assistantText(23_039, sessionId, 'reply-text-1', 'FIRST DONE banana'), + resultFrame(23_048, sessionId, 'result-1', { + userMessageUuids: [first, steer], + durationMs: 22_845, + numTurns: 4 + }), + sessionIdle(23_049, sessionId) + ] +} + +/** p2-fold-resumed: the same fold on a `--resume` session; steer at 4458. */ +export const FOLD_RESUMED_SEND_AT = { first: 12, steer: 4_458 } +export function foldResumedCapture({ + sessionId, + first, + steer +}: FoldCaptureIds): CapturedFoldFrame[] { + return [ + initFrame(218, sessionId), + userReplay(2_057, sessionId, first, FIRST_PROMPT), + assistant( + 3_229, + sessionId, + 'reply-thinking-1', + [{ type: 'thinking', thinking: 'plan' }], + [first] + ), + assistantToolUse(3_258, sessionId, 'reply-tool-1', 'toolu_sleep_1'), + toolResult(8_464, sessionId, 'tool-result-1', 'toolu_sleep_1'), + userReplay(8_467, sessionId, steer, STEER_PROMPT), + assistantToolUse(10_318, sessionId, 'reply-tool-2', 'toolu_sleep_2'), + toolResult(15_373, sessionId, 'tool-result-2', 'toolu_sleep_2'), + assistantText(23_587, sessionId, 'reply-text-1', 'FIRST DONE banana'), + resultFrame(23_628, sessionId, 'result-1', { + userMessageUuids: [first, steer], + durationMs: 23_367, + numTurns: 4 + }), + sessionIdle(23_640, sessionId) + ] +} + +/** p2-two-steers: steers at 3630 and 5031, both replayed back to back mid-turn. */ +export const TWO_STEERS_SEND_AT = { first: 31, steer: 3_630, secondSteer: 5_031 } +export function twoSteersCapture({ + sessionId, + first, + steer, + secondSteer +}: Required): CapturedFoldFrame[] { + return [ + initFrame(351, sessionId), + userReplay(1_169, sessionId, first, FIRST_PROMPT), + assistantToolUse(2_412, sessionId, 'reply-tool-1', 'toolu_sleep_1', [first]), + toolResult(7_949, sessionId, 'tool-result-1', 'toolu_sleep_1'), + userReplay(7_963, sessionId, steer, STEER_PROMPT), + userReplay(7_967, sessionId, secondSteer, SECOND_STEER_PROMPT), + assistantToolUse(9_806, sessionId, 'reply-tool-2', 'toolu_sleep_2'), + toolResult(14_835, sessionId, 'tool-result-2', 'toolu_sleep_2'), + assistantText(22_210, sessionId, 'reply-text-1', 'FIRST DONE banana mango'), + resultFrame(22_317, sessionId, 'result-1', { + userMessageUuids: [first, steer, secondSteer], + durationMs: 21_859, + numTurns: 4 + }), + sessionIdle(22_341, sessionId) + ] +} + +/** p2-miss: the steer lands too late to fold — its replay trails the first + * result, and it runs as its own turn with its own result. */ +export const MISS_SEND_AT = { first: 30, steer: 23_384 } +export function missCapture({ sessionId, first, steer }: FoldCaptureIds): { + beforeSteerSend: CapturedFoldFrame[] + afterSteerSend: CapturedFoldFrame[] +} { + return { + beforeSteerSend: [ + initFrame(333, sessionId), + userReplay(2_118, sessionId, first, FIRST_PROMPT), + assistantToolUse(2_793, sessionId, 'reply-tool-1', 'toolu_sleep_1', [first]), + toolResult(8_258, sessionId, 'tool-result-1', 'toolu_sleep_1'), + assistantToolUse(11_019, sessionId, 'reply-tool-2', 'toolu_sleep_2'), + toolResult(15_961, sessionId, 'tool-result-2', 'toolu_sleep_2'), + assistantToolUse(18_362, sessionId, 'reply-tool-3', 'toolu_sleep_3'), + toolResult(23_375, sessionId, 'tool-result-3', 'toolu_sleep_3') + ], + afterSteerSend: [ + assistantText(25_550, sessionId, 'reply-text-1', 'FIRST DONE'), + resultFrame(25_556, sessionId, 'result-1', { + userMessageUuids: [first], + durationMs: 25_241, + numTurns: 4 + }), + initFrame(25_584, sessionId), + userReplay(28_703, sessionId, steer, STEER_PROMPT), + assistantText(29_239, sessionId, 'reply-text-2', 'banana'), + resultFrame(29_275, sessionId, 'result-2', { + userMessageUuids: [steer], + durationMs: 3_711, + numTurns: 1 + }), + sessionIdle(29_306, sessionId) + ] + } +} + +/** p2-cancel: the steer is cancelled while queued — no replay ever arrives, the + * interrupt injects a synthetic user text with NO `isReplay`, and the error + * result names only the first send. */ +export const CANCEL_SEND_AT = { first: 18, steer: 3_578 } +export function cancelCapture({ sessionId, first, steer: _steer }: FoldCaptureIds): { + beforeSteerSend: CapturedFoldFrame[] + afterInterrupt: CapturedFoldFrame[] +} { + return { + beforeSteerSend: [ + initFrame(354, sessionId), + userReplay(1_329, sessionId, first, FIRST_PROMPT), + assistantToolUse(2_366, sessionId, 'reply-tool-1', 'toolu_sleep_1', [first]) + ], + afterInterrupt: [ + toolResult( + 3_889, + sessionId, + 'tool-result-1', + 'toolu_sleep_1', + "The user doesn't want to proceed with this tool use.", + true + ), + { + at: 3_900, + frame: { + type: 'user', + session_id: sessionId, + parent_tool_use_id: null, + uuid: 'interrupt-notice-1', + message: { + role: 'user', + content: [{ type: 'text', text: '[Request interrupted by user for tool use]' }] + } + } + }, + resultFrame(3_926, sessionId, 'result-1', { + userMessageUuids: [first], + durationMs: 3_573, + numTurns: 3, + subtype: 'error_during_execution', + isError: true, + terminalReason: 'aborted_tools' + }), + sessionIdle(3_941, sessionId) + ] + } +} + +/** p3-early-steer: the steer is written BEFORE the first send's replay arrives + * (send at 312, first replay at 1208) and the CLI still folds it — one init, + * one turn, one result naming both sends. */ +export const EARLY_STEER_SEND_AT = { first: 11, steer: 312 } +export function earlySteerCapture({ + sessionId, + first, + steer +}: FoldCaptureIds): CapturedFoldFrame[] { + return [ + initFrame(205, sessionId), + userReplay(1_208, sessionId, first, FIRST_PROMPT), + assistantToolUse(2_190, sessionId, 'reply-tool-1', 'toolu_sleep_1', [first]), + toolResult(6_316, sessionId, 'tool-result-1', 'toolu_sleep_1'), + userReplay(6_318, sessionId, steer, STEER_PROMPT), + assistantText(7_336, sessionId, 'reply-text-1', 'FIRST DONE banana'), + resultFrame(7_340, sessionId, 'result-1', { + userMessageUuids: [first, steer], + durationMs: 7_155, + numTurns: 2 + }), + sessionIdle(7_341, sessionId) + ] +} + +/** p3-background-wake: after the turn's result, the finished background task + * wakes the CLI — a NEW cycle: its own init, output with no user replay, and a + * result that names no send at all (`user_message_uuids` absent). The task's + * completion frames arrive BEFORE the wake's init. */ +export function backgroundWakeCapture({ sessionId, first }: FoldCaptureIds): { + firstTurn: CapturedFoldFrame[] + wake: CapturedFoldFrame[] +} { + return { + firstTurn: [ + initFrame(252, sessionId), + userReplay(1_193, sessionId, first, FIRST_PROMPT), + assistantToolUse(2_372, sessionId, 'reply-tool-1', 'toolu_bg_1', [first]), + taskFrame(2_530, sessionId, 'task_started', { + task_id: 'task_bg_1', + tool_use_id: 'toolu_bg_1', + description: 'Sleep then print marker', + is_backgrounded: true, + task_type: 'local_bash' + }), + toolResult(2_532, sessionId, 'tool-result-1', 'toolu_bg_1'), + assistantText(3_403, sessionId, 'reply-text-1', 'STARTED'), + resultFrame(3_406, sessionId, 'result-1', { + userMessageUuids: [first], + durationMs: 3_177, + numTurns: 2 + }), + sessionIdle(3_407, sessionId) + ], + wake: [ + taskFrame(17_547, sessionId, 'task_updated', { + task_id: 'task_bg_1', + patch: { status: 'completed' } + }), + taskFrame(17_547, sessionId, 'task_notification', { + task_id: 'task_bg_1', + tool_use_id: 'toolu_bg_1', + status: 'completed', + summary: 'Background command completed (exit code 0)' + }), + initFrame(17_626, sessionId), + assistantText(19_721, sessionId, 'wake-text-1', 'The background command finished.'), + resultFrame(19_729, sessionId, 'result-2', { + userMessageUuids: [], + durationMs: 2_105, + numTurns: 1 + }), + sessionIdle(19_730, sessionId) + ] + } +} diff --git a/src/main/claude/claude-child-exit-proof-ladder.test.ts b/src/main/claude/claude-child-exit-proof-ladder.test.ts new file mode 100644 index 00000000000..2d8da7f629d --- /dev/null +++ b/src/main/claude/claude-child-exit-proof-ladder.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from 'vitest' +import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../codex/codex-app-server-posix-supervisor' +import type { ClaudeChildTreeReaper } from './claude-agent-sdk-exit-proof' +import { proveClaudeChildExitWithReaper } from './claude-child-exit-proof-ladder' + +function fakeTree(): ClaudeChildTreeReaper & { reap: ReturnType } { + return { + capture: vi.fn(async () => {}), + refresh: vi.fn(async () => {}), + reap: vi.fn(async () => 'exited' as const), + treeVerdict: 'exited' + } +} + +/** A root that leaves only once a SIGTERM has had `stopMs` to act, the way a supervisor does. */ +function rootStoppedBySigterm(stopMs: number) { + let exited = false + let settle = (): void => {} + const exitPromise = new Promise((resolve) => { + settle = resolve + }) + const kill = vi.fn((signal?: NodeJS.Signals | number) => { + if (signal === 'SIGTERM') { + setTimeout(() => { + exited = true + settle() + }, stopMs) + } + return true + }) + const stdin = { end: vi.fn() } + return { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The ladder reads only pid, kill and stdin.end from its child. + child: { pid: 4321, kill, stdin } as unknown as Parameters< + typeof proveClaudeChildExitWithReaper + >[0]['child'], + kill, + stdin, + exitPromise, + exited: () => exited + } +} + +describe('Claude child exit proof ladder', () => { + it('stops a supervised child with SIGTERM and waits out the supervisor stop before forcing', async () => { + // Slower than the unsupervised 1.5 s grace, still inside the supervisor's own bound. + const root = rootStoppedBySigterm(PROVIDER_SUPERVISOR_MAX_STOP_MS - 500) + const tree = fakeTree() + + await expect( + proveClaudeChildExitWithReaper({ ...root, supervised: true, tree }, () => tree) + ).resolves.toBe(true) + + expect(root.stdin.end).toHaveBeenCalled() + expect(root.kill).toHaveBeenCalledWith('SIGTERM') + // Forcing here would SIGKILL the supervisor mid-stop and orphan Claude in its own group. + expect(root.kill).not.toHaveBeenCalledWith('SIGKILL') + expect(tree.reap).not.toHaveBeenCalled() + }, 10_000) + + it('never signals an unsupervised child for the graceful stop', async () => { + const root = rootStoppedBySigterm(0) + const tree = fakeTree() + + await proveClaudeChildExitWithReaper({ ...root, tree }, () => tree) + + // On Windows a direct SIGTERM is TerminateProcess: stdin end stays the only graceful rung. + expect(root.stdin.end).toHaveBeenCalled() + expect(root.kill).not.toHaveBeenCalledWith('SIGTERM') + expect(tree.reap).toHaveBeenCalled() + }, 10_000) +}) diff --git a/src/main/claude/claude-child-exit-proof-ladder.ts b/src/main/claude/claude-child-exit-proof-ladder.ts index 85ed629f1b9..37d4e47508d 100644 --- a/src/main/claude/claude-child-exit-proof-ladder.ts +++ b/src/main/claude/claude-child-exit-proof-ladder.ts @@ -1,8 +1,11 @@ import type { SpawnedProcess } from '../../shared/child-process/run-process' import { waitForProcessExitUntil } from '../codex/codex-process-exit-deadline' +import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../codex/codex-app-server-posix-supervisor' import type { ClaudeChildTreeReaper } from './claude-agent-sdk-exit-proof' -const GRACEFUL_EXIT_MS = 1_500 +export const GRACEFUL_EXIT_MS = 1_500 +// A signalled supervisor escalates on its own; forcing it sooner kills it and orphans Claude. +export const SUPERVISED_GRACEFUL_EXIT_MS = PROVIDER_SUPERVISOR_MAX_STOP_MS + 500 const FORCED_EXIT_MS = 1_000 export type ClaudeChildExitProofInput = { @@ -10,6 +13,8 @@ export type ClaudeChildExitProofInput = { exitPromise: Promise exited: () => boolean tree?: ClaudeChildTreeReaper + /** The child is the POSIX provider supervisor: SIGTERM stops Claude, which reaps its tools. */ + supervised?: boolean } export async function proveClaudeChildExitWithReaper( @@ -17,16 +22,24 @@ export async function proveClaudeChildExitWithReaper( createTree: () => ClaudeChildTreeReaper ): Promise { const tree = input.tree ?? createTree() - // Arm before stdin closes: only a live root can identify its descendants. + // Arm before the stop: only a live root can identify its descendants. await tree.capture() try { input.child.stdin?.end() } catch { // The reap below still owns the process. } + // Stdin end alone lets Claude finish its turn, tools and edits included; a close is a stop. + // Windows has no supervisor, and a direct SIGTERM there is TerminateProcess. + if (input.supervised && !input.exited()) { + input.child.kill('SIGTERM') + } let reaped = false if (!input.exited()) { - await waitForProcessExitUntil(input.exitPromise, GRACEFUL_EXIT_MS) + await waitForProcessExitUntil( + input.exitPromise, + input.supervised ? SUPERVISED_GRACEFUL_EXIT_MS : GRACEFUL_EXIT_MS + ) if (!input.exited()) { reaped = true await tree.refresh?.() diff --git a/src/main/claude/claude-child-root-termination.ts b/src/main/claude/claude-child-root-termination.ts index bed422532e1..bba29919578 100644 --- a/src/main/claude/claude-child-root-termination.ts +++ b/src/main/claude/claude-child-root-termination.ts @@ -19,6 +19,9 @@ type RootTerminationInput = { * nothing. A probe here could only let an unreadable process table cost the tree * the one fallback that still works once every table read has failed. * + * On POSIX the root is the provider supervisor, killed only after its own stop had + * its whole bound; Claude, in its own group, is reached by the descendant kill. + * * False means no signal was sent, because the root had already left. */ export function terminateClaudeRoot(input: RootTerminationInput): boolean { diff --git a/src/main/claude/claude-fold-steer-frame-builders.test-fixture.ts b/src/main/claude/claude-fold-steer-frame-builders.test-fixture.ts new file mode 100644 index 00000000000..efb5781bad8 --- /dev/null +++ b/src/main/claude/claude-fold-steer-frame-builders.test-fixture.ts @@ -0,0 +1,184 @@ +// The frame shapes the fold-steer captures are rebuilt from, keeping only the +// fields the dispatch/turn path reads. + +export type CapturedFoldFrame = { at: number; frame: Record } + +export type FoldCaptureIds = { + /** The provider session every frame names. */ + sessionId: string + /** Client uuid of the turn-opening send; the CLI adopts it on the replay. */ + first: string + /** Client uuid of the mid-turn send. */ + steer: string + /** Client uuid of the second mid-turn send (two-steers only). */ + secondSteer?: string +} + +export function userReplay( + at: number, + sessionId: string, + uuid: string, + text: string +): CapturedFoldFrame { + return { + at, + frame: { + type: 'user', + session_id: sessionId, + parent_tool_use_id: null, + uuid, + isReplay: true, + message: { role: 'user', content: [{ type: 'text', text }] } + } + } +} + +export function assistant( + at: number, + sessionId: string, + uuid: string, + content: unknown[], + userMessageUuids?: string[] +): CapturedFoldFrame { + return { + at, + frame: { + type: 'assistant', + session_id: sessionId, + parent_tool_use_id: null, + uuid, + message: { id: `msg-${uuid}`, role: 'assistant', content }, + // Only the reply that directly answers a send carries the correlation. + ...(userMessageUuids && userMessageUuids.length > 0 + ? { user_message_uuid: userMessageUuids[0], user_message_uuids: userMessageUuids } + : {}) + } + } +} + +export function assistantToolUse( + at: number, + sessionId: string, + uuid: string, + toolUseId: string, + userMessageUuids?: string[] +): CapturedFoldFrame { + return assistant( + at, + sessionId, + uuid, + [{ type: 'tool_use', id: toolUseId, name: 'Bash', input: { command: 'sleep 5' } }], + userMessageUuids + ) +} + +export function assistantText( + at: number, + sessionId: string, + uuid: string, + text: string, + userMessageUuids?: string[] +): CapturedFoldFrame { + return assistant(at, sessionId, uuid, [{ type: 'text', text }], userMessageUuids) +} + +export function toolResult( + at: number, + sessionId: string, + uuid: string, + toolUseId: string, + content = '(Bash completed with no output)', + isError = false +): CapturedFoldFrame { + return { + at, + frame: { + type: 'user', + session_id: sessionId, + parent_tool_use_id: null, + uuid, + message: { + role: 'user', + content: [{ type: 'tool_result', tool_use_id: toolUseId, content, is_error: isError }] + } + } + } +} + +export function resultFrame( + at: number, + sessionId: string, + uuid: string, + fields: { + userMessageUuids: string[] + durationMs: number + numTurns: number + subtype?: string + isError?: boolean + terminalReason?: string + } +): CapturedFoldFrame { + return { + at, + frame: { + type: 'result', + subtype: fields.subtype ?? 'success', + session_id: sessionId, + uuid, + is_error: fields.isError ?? false, + terminal_reason: fields.terminalReason ?? 'completed', + duration_ms: fields.durationMs, + num_turns: fields.numTurns, + result: 'FIRST DONE', + ...(fields.userMessageUuids.length > 0 + ? { + user_message_uuid: fields.userMessageUuids[0], + user_message_uuids: fields.userMessageUuids + } + : {}) + } + } +} + +export function sessionIdle(at: number, sessionId: string): CapturedFoldFrame { + return { + at, + frame: { + type: 'system', + subtype: 'session_state_changed', + state: 'idle', + session_id: sessionId, + uuid: `ssc-idle-${at}` + } + } +} + +/** Root `system/init`: the CLI starting a request cycle. Emitted at startup and + * again for every later cycle — sequential turn, queued turn, background wake, + * /compact (p3 captures). */ +export function initFrame(at: number, sessionId: string): CapturedFoldFrame { + return { + at, + frame: { + type: 'system', + subtype: 'init', + session_id: sessionId, + uuid: `init-${at}`, + model: 'claude-sonnet-5', + apiKeySource: 'none' + } + } +} + +/** A root background-task system frame (`task_started`, `task_updated`, ...). */ +export function taskFrame( + at: number, + sessionId: string, + subtype: string, + fields: Record +): CapturedFoldFrame { + return { + at, + frame: { type: 'system', subtype, session_id: sessionId, uuid: `${subtype}-${at}`, ...fields } + } +} diff --git a/src/main/claude/claude-fold-steer-receipt.test.ts b/src/main/claude/claude-fold-steer-receipt.test.ts new file mode 100644 index 00000000000..4726dc55d82 --- /dev/null +++ b/src/main/claude/claude-fold-steer-receipt.test.ts @@ -0,0 +1,622 @@ +// A send the CLI folds into the running request cycle: its adopted replay is a +// delivery receipt, never a new turn boundary. The provider's own cycle state +// decides — a root init announces each new cycle, and a result's +// `user_message_uuids` names every send the cycle ran. The measured miss, a +// lost result followed by a new cycle, and fresh replay uuids all keep the +// replay-driven opener path. Captured orders +// from Claude CLI 2.1.280 (`claude-captured-fold-steer-frames.test-fixture.ts`). + +import { describe, expect, it, vi, type Mock } from 'vitest' +import type { + AgentJournalItemBody, + AgentJournalItemIdentity, + AgentJournalMessageItem +} from '../../shared/agent-session-journal-types' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { readAgentJournalTurn } from '../../shared/agent-session-turn-record' +import { ClaudeStructuredSessionAdapter } from './claude-structured-session-adapter' +import type { + ClaudeStructuredSessionAdapterDeps, + ClaudeStructuredSessionEvent +} from './claude-structured-session-state' +import type { ClaudeStructuredLaunch } from './claude-structured-launch-resolution' +import { + fakeClaude, + identityFor, + PROVIDER_SESSION_ID +} from './claude-structured-session-test-support' +import { + assistantText, + initFrame, + resultFrame, + userReplay, + type CapturedFoldFrame +} from './claude-fold-steer-frame-builders.test-fixture' +import { + backgroundWakeCapture, + cancelCapture, + CANCEL_SEND_AT, + earlySteerCapture, + EARLY_STEER_SEND_AT, + FIRST_PROMPT, + foldFreshCapture, + FOLD_FRESH_SEND_AT, + foldResumedCapture, + FOLD_RESUMED_SEND_AT, + missCapture, + MISS_SEND_AT, + SECOND_STEER_PROMPT, + STEER_PROMPT, + twoSteersCapture, + TWO_STEERS_SEND_AT +} from './claude-captured-fold-steer-frames.test-fixture' + +const T0 = 1_700_000_100_000 + +type Rig = { + adapter: ClaudeStructuredSessionAdapter + claude: ReturnType + events: ClaudeStructuredSessionEvent[] + settled: Mock + /** Every revision of every turn lifecycle row, in append order. */ + turns: () => NonNullable>[] + deliver: (captured: CapturedFoldFrame) => void + /** Dispatches at the captured send offset and returns the client uuid the CLI adopts. */ + dispatchAt: (at: number, clientMessageId: string, text: string) => Promise +} + +async function riggedAdapter( + launch: Partial = {}, + claudeOptions: Parameters[0] = {} +): Promise { + let nowMs = T0 + const appended: { identity: AgentJournalItemIdentity; body: AgentJournalItemBody }[] = [] + const sink: StructuredAgentSessionEventSink = { + appendItem: (identity, body) => appended.push({ identity, body }), + appendTombstone: () => {}, + publish: () => {} + } + const events: ClaudeStructuredSessionEvent[] = [] + const settled = vi.fn() + const claude = fakeClaude({ replayUuid: null, ...claudeOptions }) + const deps: ClaudeStructuredSessionAdapterDeps = { + resolveLaunch: async () => ({ + pathToClaudeCodeExecutable: 'claude', + options: {}, + cwd: '/work/repo', + claudeConfigDir: '/accounts/claude', + providerSessionId: PROVIDER_SESSION_ID, + resumeLeafUuid: null, + resumesTranscript: false, + continuesChain: false, + ...launch + }), + onEvent: (event) => events.push(event), + openConnection: claude.openConnection, + readProcessStartTime: async () => T0 - 1_000, + now: () => nowMs, + persistHandle: async () => {}, + onDispatchSettledLate: settled + } + const adapter = new ClaudeStructuredSessionAdapter(deps) + await adapter.acquire({ identity: identityFor(), fence: 7, spawnToken: 'spawn-9', events: sink }) + await adapter.awaitStarted('session-1') + return { + adapter, + claude, + events, + settled, + turns: () => + appended.flatMap((item) => { + const turn = readAgentJournalTurn(item.body) + return turn ? [turn] : [] + }), + deliver: (captured) => { + nowMs = T0 + captured.at + claude.connections[0]!.handlers.onMessage?.(captured.frame) + }, + dispatchAt: async (at, clientMessageId, text) => { + nowMs = T0 + at + const body: AgentJournalMessageItem = { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text }] + } + const outcome = await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId, + body, + requestedAt: nowMs, + fence: 7 + }) + expect(outcome).toEqual({ state: 'admitted' }) + const sentUuid = claude.connections[0]!.sent.at(-1)?.uuid + if (typeof sentUuid !== 'string') { + throw new Error('the fake connection recorded no sent uuid') + } + return sentUuid + } + } +} + +function replayEventFor(events: ClaudeStructuredSessionEvent[], uuid: string) { + return events.find( + (event) => + event.type === 'message' && event.message.type === 'user' && event.message.uuid === uuid + ) +} + +describe('Claude fold receipt for a mid-turn send (captured orders)', () => { + it('fold-fresh: the folded steer settles as a receipt inside the one turn it was sent during', async () => { + const rig = await riggedAdapter() + const first = await rig.dispatchAt(FOLD_FRESH_SEND_AT.first, 'client-first', FIRST_PROMPT) + const framesFor = (steerUuid: string) => + foldFreshCapture({ sessionId: PROVIDER_SESSION_ID, first, steer: steerUuid }) + for (const captured of framesFor('pending')) { + if (captured.at < FOLD_FRESH_SEND_AT.steer) { + rig.deliver(captured) + } + } + const steer = await rig.dispatchAt(FOLD_FRESH_SEND_AT.steer, 'client-steer', STEER_PROMPT) + for (const captured of framesFor(steer)) { + if (captured.at > FOLD_FRESH_SEND_AT.steer) { + rig.deliver(captured) + } + } + + // ONE turn record for the whole run, never marked interrupted. + const turnIds = [...new Set(rig.turns().map((turn) => turn.turnId))] + expect(turnIds).toEqual([first]) + expect(rig.turns().every((turn) => turn.state !== 'interrupted')).toBe(true) + // The settled duration is the single turn's, spanning both sends' work. + expect(rig.turns().at(-1)).toMatchObject({ + turnId: first, + state: 'completed', + outcome: 'success', + startedAt: T0 + 2_094, + completedAt: T0 + 23_048, + durationMs: 22_845 + }) + // The steer's replay is a receipt: delivery settles under the steer's own + // provider identity, which is what attributes the user item inside the turn. + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-steer', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: steer } + }) + // And it opens no boundary: the replay event carries no startsTurn. + expect(replayEventFor(rig.events, steer)).not.toHaveProperty('startsTurn') + expect(replayEventFor(rig.events, first)).toMatchObject({ startsTurn: true }) + }) + + it('session-start proof: a hook-frame startup proof with init only at the first cycle still folds', async () => { + // Live sessions prove the session from a SessionStart hook frame BEFORE + // system/init arrives (measured against the real CLI); nothing about the + // fold may depend on fields of the startup proof frame. + const rig = await riggedAdapter({}, { initProof: 'session-start' }) + const first = await rig.dispatchAt(FOLD_FRESH_SEND_AT.first, 'client-first', FIRST_PROMPT) + const framesFor = (steerUuid: string) => + foldFreshCapture({ sessionId: PROVIDER_SESSION_ID, first, steer: steerUuid }) + for (const captured of framesFor('pending')) { + if (captured.at < FOLD_FRESH_SEND_AT.steer) { + rig.deliver(captured) + } + } + const steer = await rig.dispatchAt(FOLD_FRESH_SEND_AT.steer, 'client-steer', STEER_PROMPT) + for (const captured of framesFor(steer)) { + if (captured.at > FOLD_FRESH_SEND_AT.steer) { + rig.deliver(captured) + } + } + + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([first]) + expect(rig.turns().every((turn) => turn.state !== 'interrupted')).toBe(true) + expect(replayEventFor(rig.events, steer)).not.toHaveProperty('startsTurn') + }) + + it('fold-resumed: the receipt holds on a resumed provider session', async () => { + const rig = await riggedAdapter({ + resumeLeafUuid: 'leaf-1', + resumesTranscript: true, + continuesChain: true + }) + const first = await rig.dispatchAt(FOLD_RESUMED_SEND_AT.first, 'client-first', FIRST_PROMPT) + const framesFor = (steerUuid: string) => + foldResumedCapture({ sessionId: PROVIDER_SESSION_ID, first, steer: steerUuid }) + for (const captured of framesFor('pending')) { + if (captured.at < FOLD_RESUMED_SEND_AT.steer) { + rig.deliver(captured) + } + } + const steer = await rig.dispatchAt(FOLD_RESUMED_SEND_AT.steer, 'client-steer', STEER_PROMPT) + for (const captured of framesFor(steer)) { + if (captured.at > FOLD_RESUMED_SEND_AT.steer) { + rig.deliver(captured) + } + } + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-steer', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: steer } + }) + + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([first]) + expect(rig.turns().every((turn) => turn.state !== 'interrupted')).toBe(true) + expect(rig.turns().at(-1)).toMatchObject({ + turnId: first, + state: 'completed', + durationMs: 23_367 + }) + }) + + it('two-steers: both folded sends settle individually inside the one turn', async () => { + const rig = await riggedAdapter() + const first = await rig.dispatchAt(TWO_STEERS_SEND_AT.first, 'client-first', FIRST_PROMPT) + const framesFor = (steerUuid: string, secondSteerUuid: string) => + twoSteersCapture({ + sessionId: PROVIDER_SESSION_ID, + first, + steer: steerUuid, + secondSteer: secondSteerUuid + }) + for (const captured of framesFor('pending', 'pending-2')) { + if (captured.at < TWO_STEERS_SEND_AT.steer) { + rig.deliver(captured) + } + } + // Both steers go out mid-turn, before the next captured frame at 7949. + const steer = await rig.dispatchAt(TWO_STEERS_SEND_AT.steer, 'client-steer', STEER_PROMPT) + const secondSteer = await rig.dispatchAt( + TWO_STEERS_SEND_AT.secondSteer, + 'client-steer-2', + SECOND_STEER_PROMPT + ) + for (const captured of framesFor(steer, secondSteer)) { + if (captured.at > TWO_STEERS_SEND_AT.secondSteer) { + rig.deliver(captured) + } + } + + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([first]) + expect(rig.turns().every((turn) => turn.state !== 'interrupted')).toBe(true) + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-steer', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: steer } + }) + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-steer-2', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: secondSteer } + }) + }) + + it('early-steer: a steer written before the first replay arrives still folds into the one turn', async () => { + const rig = await riggedAdapter() + const first = await rig.dispatchAt(EARLY_STEER_SEND_AT.first, 'client-first', FIRST_PROMPT) + const framesFor = (steerUuid: string) => + earlySteerCapture({ sessionId: PROVIDER_SESSION_ID, first, steer: steerUuid }) + for (const captured of framesFor('pending')) { + if (captured.at < EARLY_STEER_SEND_AT.steer) { + rig.deliver(captured) + } + } + const steer = await rig.dispatchAt(EARLY_STEER_SEND_AT.steer, 'client-steer', STEER_PROMPT) + for (const captured of framesFor(steer)) { + if (captured.at > EARLY_STEER_SEND_AT.steer) { + rig.deliver(captured) + } + } + + // No turn was open when the steer was written, and it still folded. + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([first]) + expect(rig.turns().every((turn) => turn.state !== 'interrupted')).toBe(true) + expect(rig.turns().at(-1)).toMatchObject({ + turnId: first, + state: 'completed', + durationMs: 7_155 + }) + expect(replayEventFor(rig.events, steer)).not.toHaveProperty('startsTurn') + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-steer', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: steer } + }) + }) + + it('background-wake: the wake cycle opens from its output and its result settles no waiter', async () => { + const rig = await riggedAdapter() + const first = await rig.dispatchAt(13, 'client-first', FIRST_PROMPT) + const { firstTurn, wake } = backgroundWakeCapture({ + sessionId: PROVIDER_SESSION_ID, + first, + steer: 'unused' + }) + for (const captured of [...firstTurn, ...wake]) { + rig.deliver(captured) + } + + // The task's completion revises a live row ahead of the wake's init, and + // that provider output is what opens the wake turn. + const finalByTurn = new Map(rig.turns().map((turn) => [turn.turnId, turn])) + expect([...finalByTurn.keys()]).toEqual([first, 'task_updated-17547']) + expect(finalByTurn.get('task_updated-17547')).toMatchObject({ state: 'completed' }) + // The wake result names no send; only the first send ever settled. + expect(rig.settled).toHaveBeenCalledTimes(1) + }) + + it('background-wake: a steer replayed after the wake cycle began work folds into the wake turn', async () => { + const rig = await riggedAdapter() + const first = await rig.dispatchAt(13, 'client-first', FIRST_PROMPT) + const { firstTurn, wake } = backgroundWakeCapture({ + sessionId: PROVIDER_SESSION_ID, + first, + steer: 'pending' + }) + const wakeResult = wake.findIndex((captured) => captured.frame.type === 'result') + for (const captured of [...firstTurn, ...wake.slice(0, wakeResult)]) { + rig.deliver(captured) + } + // Captured order up to the wake's output; the steer's replay is placed + // mid-cycle, where the CLI replays a send it folded (p3-early-steer). + const steer = await rig.dispatchAt(19_725, 'client-steer', STEER_PROMPT) + rig.deliver(userReplay(19_726, PROVIDER_SESSION_ID, steer, STEER_PROMPT)) + for (const captured of wake.slice(wakeResult)) { + rig.deliver(captured) + } + + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([ + first, + 'task_updated-17547' + ]) + expect(rig.turns().every((turn) => turn.state !== 'interrupted')).toBe(true) + expect(replayEventFor(rig.events, steer)).not.toHaveProperty('startsTurn') + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-steer', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: steer } + }) + }) + + it('mid-turn auto-compaction emits no root init, so a steer after the boundary still folds', async () => { + // Measured (p4-autocompact, CLAUDE_CODE_AUTO_COMPACT_WINDOW forced): the + // boundary is `status compacting` + `compact_boundary` + a synthetic + // continuation user frame — and NO root init, so cycle work survives it. + const rig = await riggedAdapter() + const first = await rig.dispatchAt(14, 'client-first', FIRST_PROMPT) + rig.deliver(initFrame(234, PROVIDER_SESSION_ID)) + rig.deliver(userReplay(2_437, PROVIDER_SESSION_ID, first, FIRST_PROMPT)) + rig.deliver(assistantText(2_864, PROVIDER_SESSION_ID, 'reply-1', 'reading files')) + rig.deliver({ + at: 11_518, + frame: { + type: 'system', + subtype: 'status', + status: 'compacting', + session_id: PROVIDER_SESSION_ID, + uuid: 'status-compacting-1' + } + }) + rig.deliver({ + at: 26_584, + frame: { + type: 'system', + subtype: 'compact_boundary', + session_id: PROVIDER_SESSION_ID, + uuid: 'compact-boundary-1', + compact_metadata: { trigger: 'auto', pre_tokens: 69_960, post_tokens: 9_311 } + } + }) + rig.deliver({ + at: 26_585, + frame: { + type: 'user', + session_id: PROVIDER_SESSION_ID, + parent_tool_use_id: null, + uuid: 'continuation-1', + isSynthetic: true, + message: { + role: 'user', + content: [{ type: 'text', text: 'This session is being continued.' }] + } + } + }) + const steer = await rig.dispatchAt(26_987, 'client-steer', STEER_PROMPT) + rig.deliver(userReplay(34_601, PROVIDER_SESSION_ID, steer, STEER_PROMPT)) + rig.deliver(assistantText(37_824, PROVIDER_SESSION_ID, 'reply-2', 'FIRST DONE banana')) + rig.deliver( + resultFrame(37_828, PROVIDER_SESSION_ID, 'result-1', { + userMessageUuids: [first, steer], + durationMs: 37_616, + numTurns: 6 + }) + ) + + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([first]) + expect(rig.turns().every((turn) => turn.state !== 'interrupted')).toBe(true) + expect(replayEventFor(rig.events, steer)).not.toHaveProperty('startsTurn') + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-steer', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: steer } + }) + }) + + it('miss: a steer whose replay trails the result keeps the opener path and its own turn', async () => { + const rig = await riggedAdapter() + const first = await rig.dispatchAt(MISS_SEND_AT.first, 'client-first', FIRST_PROMPT) + const { beforeSteerSend } = missCapture({ + sessionId: PROVIDER_SESSION_ID, + first, + steer: 'pending' + }) + for (const captured of beforeSteerSend) { + rig.deliver(captured) + } + const steer = await rig.dispatchAt(MISS_SEND_AT.steer, 'client-steer', STEER_PROMPT) + for (const captured of missCapture({ sessionId: PROVIDER_SESSION_ID, first, steer }) + .afterSteerSend) { + rig.deliver(captured) + } + + // Two real turns: the first completed by its result — not interrupted — + // and the late steer's own turn opened by its replay. + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([first, steer]) + expect(rig.turns().every((turn) => turn.state !== 'interrupted')).toBe(true) + const finalByTurn = new Map(rig.turns().map((turn) => [turn.turnId, turn])) + expect(finalByTurn.get(first)).toMatchObject({ + state: 'completed', + completedAt: T0 + 25_556, + durationMs: 25_241 + }) + expect(finalByTurn.get(steer)).toMatchObject({ + state: 'completed', + startedAt: T0 + 28_703, + completedAt: T0 + 29_275, + durationMs: 3_711 + }) + expect(replayEventFor(rig.events, steer)).toMatchObject({ startsTurn: true }) + }) + + it('cancel: a cancelled steer settles nothing and the receipt path changes none of it', async () => { + const rig = await riggedAdapter() + const first = await rig.dispatchAt(CANCEL_SEND_AT.first, 'client-first', FIRST_PROMPT) + const { beforeSteerSend, afterInterrupt } = cancelCapture({ + sessionId: PROVIDER_SESSION_ID, + first, + steer: 'never-replayed' + }) + for (const captured of beforeSteerSend) { + rig.deliver(captured) + } + await rig.dispatchAt(CANCEL_SEND_AT.steer, 'client-steer', STEER_PROMPT) + for (const captured of afterInterrupt) { + rig.deliver(captured) + } + + // One turn, ended by the interrupt's error result exactly as before. + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([first]) + expect(rig.turns().at(-1)).toMatchObject({ + turnId: first, + state: 'interrupted', + outcome: 'cancellation' + }) + // The steer was never replayed and its result never named it: no settlement. + expect(rig.settled).not.toHaveBeenCalledWith( + expect.objectContaining({ clientMessageId: 'client-steer' }) + ) + // The interrupt's synthetic user text opens no turn either. + expect(replayEventFor(rig.events, 'interrupt-notice-1')).not.toHaveProperty('startsTurn') + }) +}) + +describe('Claude fold receipt boundaries (synthetic orders)', () => { + it('lost result: a root init starts a cycle with no work yet, so the next adopted replay is a boundary', async () => { + const rig = await riggedAdapter() + const uuidA = await rig.dispatchAt(10, 'client-a', 'first prompt') + rig.deliver(userReplay(1_000, PROVIDER_SESSION_ID, uuidA, 'first prompt')) + const uuidB = await rig.dispatchAt(1_500, 'client-b', STEER_PROMPT) + // A's result never arrives, but the CLI announces its next request cycle: + // the open turn stops folding, so B's replay opens its own turn. + rig.deliver(initFrame(2_000, PROVIDER_SESSION_ID)) + rig.deliver(userReplay(3_000, PROVIDER_SESSION_ID, uuidB, STEER_PROMPT)) + + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([uuidA, uuidB]) + expect(replayEventFor(rig.events, uuidB)).toMatchObject({ startsTurn: true }) + }) + + it('provider wake: an adopted replay during the live wake cycle folds into the wake turn', async () => { + // Cycle semantics: a wake is its own init-led cycle (p3-background-wake) and + // a replay the CLI emits mid-cycle was folded into that cycle (p3-early-steer). + const rig = await riggedAdapter() + const uuidA = await rig.dispatchAt(10, 'client-a', 'first prompt') + rig.deliver(userReplay(1_000, PROVIDER_SESSION_ID, uuidA, 'first prompt')) + rig.deliver( + resultFrame(2_000, PROVIDER_SESSION_ID, 'result-1', { + userMessageUuids: [uuidA], + durationMs: 1_990, + numTurns: 1 + }) + ) + const uuidB = await rig.dispatchAt(2_500, 'client-b', STEER_PROMPT) + rig.deliver(initFrame(3_000, PROVIDER_SESSION_ID)) + rig.deliver(assistantText(3_500, PROVIDER_SESSION_ID, 'provider-resumed-1', 'background done')) + rig.deliver(userReplay(4_000, PROVIDER_SESSION_ID, uuidB, STEER_PROMPT)) + + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([ + uuidA, + 'provider-resumed-1' + ]) + expect(replayEventFor(rig.events, uuidB)).not.toHaveProperty('startsTurn') + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-b', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: uuidB } + }) + }) + + it('a fresh replay uuid with user_message_uuid correlation keeps the opener path, not a fold receipt', async () => { + const rig = await riggedAdapter() + const uuidA = await rig.dispatchAt(10, 'client-a', 'first prompt') + rig.deliver(userReplay(1_000, PROVIDER_SESSION_ID, uuidA, 'first prompt')) + const uuidB = await rig.dispatchAt(1_500, 'client-b', STEER_PROMPT) + // A replay that did not adopt the client uuid is not the measured fold + // shape, so it keeps the opener path it had before the receipt existed. + const fresh = userReplay(2_000, PROVIDER_SESSION_ID, 'fresh-turn-2', STEER_PROMPT) + rig.deliver({ ...fresh, frame: { ...fresh.frame, user_message_uuid: uuidB } }) + + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([uuidA, 'fresh-turn-2']) + expect(replayEventFor(rig.events, 'fresh-turn-2')).toMatchObject({ startsTurn: true }) + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-b', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: 'fresh-turn-2' } + }) + }) + + it('plural result uuids: an unsettled folded waiter settles under its own uuid, never a shared result alias', async () => { + const rig = await riggedAdapter() + const uuidA = await rig.dispatchAt(10, 'client-a', 'first prompt') + rig.deliver(userReplay(1_000, PROVIDER_SESSION_ID, uuidA, 'first prompt')) + const uuidB = await rig.dispatchAt(1_500, 'client-b', STEER_PROMPT) + // B's replay never arrives; the folded turn's one result still names it. + rig.deliver( + resultFrame(5_000, PROVIDER_SESSION_ID, 'result-1', { + userMessageUuids: [uuidA, uuidB], + durationMs: 4_990, + numTurns: 2 + }) + ) + + expect(rig.settled).toHaveBeenCalledWith({ + sessionId: 'session-1', + clientMessageId: 'client-b', + providerIdentity: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, uuid: uuidB } + }) + expect(rig.settled).not.toHaveBeenCalledWith( + expect.objectContaining({ providerIdentity: expect.objectContaining({ uuid: 'result-1' }) }) + ) + // The correlated result opened nothing and the one turn completed normally. + expect([...new Set(rig.turns().map((turn) => turn.turnId))]).toEqual([uuidA]) + expect(rig.turns().at(-1)).toMatchObject({ turnId: uuidA, state: 'completed' }) + }) + + it('plural result uuids: a result naming only settled sends does not claim an unnamed live waiter by queue order', async () => { + const rig = await riggedAdapter() + const uuidA = await rig.dispatchAt(10, 'client-a', 'first prompt') + rig.deliver(userReplay(1_000, PROVIDER_SESSION_ID, uuidA, 'first prompt')) + await rig.dispatchAt(1_500, 'client-b', STEER_PROMPT) + // A result that names only sends already settled must not fall back to + // queue order and claim B, the still-live waiter it did not name. + rig.deliver( + resultFrame(5_000, PROVIDER_SESSION_ID, 'result-1', { + userMessageUuids: [uuidA], + durationMs: 4_990, + numTurns: 1 + }) + ) + expect(rig.settled).not.toHaveBeenCalledWith( + expect.objectContaining({ clientMessageId: 'client-b' }) + ) + }) +}) diff --git a/src/main/claude/claude-journal-translator-contract.ts b/src/main/claude/claude-journal-translator-contract.ts index 7a7fc4291c7..1ccf24e1393 100644 --- a/src/main/claude/claude-journal-translator-contract.ts +++ b/src/main/claude/claude-journal-translator-contract.ts @@ -13,6 +13,10 @@ export type ClaudeJournalTranslator = { /** The open turn's provider id — the same id its journal row carries, and the one * a client's Stop names. Sole owner: no reader keeps a copy to disagree with. */ readonly currentTurnId: string | null + /** True while a turn is open and the provider's current request cycle has + * done root work since its init — the state in which the CLI folds an + * arriving send into the turn. */ + readonly openTurnInLiveProviderCycle: boolean flush: () => void childToolOwner?: ClaudeChildToolQueries['childToolOwner'] childActivity?: ClaudeChildToolQueries['childActivity'] diff --git a/src/main/claude/claude-open-turn.ts b/src/main/claude/claude-open-turn.ts index 08a7916a97b..8a9dc30cd5a 100644 --- a/src/main/claude/claude-open-turn.ts +++ b/src/main/claude/claude-open-turn.ts @@ -30,6 +30,11 @@ export class ClaudeOpenTurn { * failed: nothing would ever close the turn it opened, and the row would read * working for the life of the session. Only an accepted send lifts it. */ private reopenSuppressed = false + /** Whether the provider's current request cycle has done root work — a send + * echo or model output — since its init. Output can open a turn ahead of its + * cycle's init (a background task finishing), so membership is read from the + * cycle's work, not from when the turn opened. */ + private cycleWorkObserved = false private readonly opener: ( frame: Record, source: ClaudeTurnSource | null, @@ -63,6 +68,23 @@ export class ClaudeOpenTurn { return this.current !== null } + /** Whether a turn is open inside a provider request cycle that has already + * done work — the state in which the CLI folds an arriving send into it. A + * cycle's first send is its opener, never a fold. */ + get openedInLiveProviderCycle(): boolean { + return this.current !== null && this.cycleWorkObserved + } + + /** A root init frame: the CLI is starting a new request cycle. */ + observeProviderCycleStart(): void { + this.cycleWorkObserved = false + } + + /** A root send echo or model output inside the current request cycle. */ + observeProviderCycleWork(): void { + this.cycleWorkObserved = true + } + /** Open a turn, ending whichever one was still open. A new turn starting is the * only end the previous one gets when its result never arrives; settling it * later would sweep THIS turn. */ @@ -91,6 +113,8 @@ export class ClaudeOpenTurn { /** End the open turn, if one is open, and clear the live activity line. The * context facts the end brings ride the same revision. */ settle(end: ClaudeTurnEnd, contextUsage?: AgentSessionContextUsage): void { + // Every settle is a provider cycle ending (result, idle, child exit). + this.cycleWorkObserved = false if (this.current) { this.publish(this.current, end, contextUsage) this.current = null diff --git a/src/main/claude/claude-real-cli-availability-test-support.ts b/src/main/claude/claude-real-cli-availability-test-support.ts new file mode 100644 index 00000000000..faa322c456b --- /dev/null +++ b/src/main/claude/claude-real-cli-availability-test-support.ts @@ -0,0 +1,64 @@ +// Whether a real, signed-in Claude CLI is present — the gate every real-CLI +// suite skips on. Probed once per test process. + +import { homedir } from 'node:os' +import { join } from 'node:path' +import { runProcessSync, type ProcessResult } from '../../shared/child-process/run-process' +import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution' +import { CLAUDE_AUTH_ENV_VARS } from '../claude-accounts/environment' +import { resolveClaudeCommand } from '../codex-cli/command' + +export const realClaudeCommand = resolveClaudeCommand() + +// Why paired: the probe must run the CLI under the same node the structured launch gives it. +function probeRealClaude(args: string[]): ProcessResult | null { + try { + return runProcessSync({ + program: realClaudeCommand, + args, + env: withCliRuntimeOnPath(realClaudeCommand, process.env), + stdio: ['ignore', 'pipe', 'pipe'], + timeoutMs: 5_000 + }) + } catch { + return null + } +} + +export const realClaudeAvailable = probeRealClaude(['--version'])?.code === 0 + +/** The CLI's own account report — the only source of truth for where it writes that + * is not derived from Orca's own path expressions. */ +export const realClaudeAuthStatus = (() => { + if (!realClaudeAvailable) { + return null + } + const result = probeRealClaude(['auth', 'status', '--json']) + if (!result || result.code !== 0) { + return null + } + try { + return JSON.parse(result.stdout) as { loggedIn?: boolean; projectsDirectory?: string } + } catch { + return null + } +})() + +export const realClaudeAuthenticated = realClaudeAuthStatus?.loggedIn === true + +/** The config dir and env auth the availability probe above saw, for a real-CLI launch. + * The connection strips an inherited CLAUDE_CONFIG_DIR and inherited auth vars, so + * without these the child silently runs against ~/.claude whatever the probe checked. */ +export function realClaudeLaunchHome(): { claudeConfigDir: string; env: Record } { + const env: Record = {} + for (const key of CLAUDE_AUTH_ENV_VARS) { + const value = process.env[key] + if (value) { + env[key] = value + } + } + return { + claudeConfigDir: process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), '.claude'), + env + } +} diff --git a/src/main/claude/claude-replay-turn-resolution.ts b/src/main/claude/claude-replay-turn-resolution.ts new file mode 100644 index 00000000000..d34d0efc206 --- /dev/null +++ b/src/main/claude/claude-replay-turn-resolution.ts @@ -0,0 +1,221 @@ +// Which dispatch a provider frame settles, and whether it opens a turn. +// +// A replay or result is joined to its waiter by the client uuid Claude echoes. +// A send Claude FOLDS into the running request cycle is replayed mid-cycle with +// the client uuid adopted: once that cycle has done work, that replay is a +// delivery receipt and opens no boundary. + +import { forgetRetiredWaiter } from './claude-structured-dispatch-waiters' +import { + claudeHasReplayContent, + readClaudeMessageEnvelope +} from './claude-structured-item-translation' +import type { + ClaudeDispatchWaiter, + ClaudeLateDispatchOutcome, + ClaudeSession +} from './claude-structured-session-state' +import { readClaudeFrameString } from './claude-structured-init-proof' +import { claudeDispatchContentKey } from './claude-structured-dispatch-content' + +/** Settles a provider-proven late outcome; replay rows independently reconcile acceptance. */ +export type ClaudeLateDispatchSettlement = (input: ClaudeLateDispatchOutcome) => void + +export type ClaudeReplayTurnOrigin = { requestedAt: number | null } + +export function resolveClaudeReplayTurn( + session: ClaudeSession, + message: Record, + onSettledLate?: ClaudeLateDispatchSettlement +): ClaudeReplayTurnOrigin | null { + const envelope = readClaudeMessageEnvelope(message) + const isUserReplay = + envelope?.role === 'user' && + message.parent_tool_use_id === null && + claudeHasReplayContent(envelope) + const isCompletedCommand = message.type === 'result' + if ( + (!isUserReplay && !isCompletedCommand) || + readClaudeFrameString(message, 'session_id') !== session.providerSessionId + ) { + return null + } + const uuid = readClaudeFrameString(message, 'uuid') + if (!uuid) { + return null + } + + // Newer SDK frames carry the client uuid that caused a turn. A correlation + // value is authoritative: never fall back to queue order or content, since + // identical prompts may be in flight across a timeout boundary. + const userMessageUuid = readClaudeFrameString(message, 'user_message_uuid') + // A folded turn's result names every send it ran under `user_message_uuids`. + // Each member settles its own waiter under its own uuid — the result frame's + // uuid is never adopted as a shared alias for multiple submissions. + const resultUserMessageUuids = isCompletedCommand ? claudeResultUserMessageUuids(message) : [] + for (const member of resultUserMessageUuids) { + if (member === userMessageUuid) { + continue + } + const live = session.dispatchWaiters.find((candidate) => candidate.sentUuid === member) + if (live) { + settleWaiter(session, live, member, onSettledLate) + continue + } + const late = session.retiredDispatchWaiters.find((candidate) => candidate.sentUuid === member) + if (late) { + forgetRetiredWaiter(session, late) + recoverLateIdentity(session, late, member, false, onSettledLate) + } + } + if (userMessageUuid) { + const exact = session.dispatchWaiters.find( + (candidate) => candidate.sentUuid === userMessageUuid + ) + if (exact) { + const foldReceipt = isUserReplay && claudeReplayIsFoldReceipt(session, exact, uuid) + settleWaiter(session, exact, uuid, onSettledLate) + return isUserReplay && !foldReceipt ? { requestedAt: exact.requestedAt } : null + } + const retired = session.retiredDispatchWaiters.find( + (candidate) => candidate.sentUuid === userMessageUuid + ) + if (retired) { + forgetRetiredWaiter(session, retired) + recoverLateIdentity(session, retired, uuid, isUserReplay, onSettledLate) + return null + } + return null + } + if (resultUserMessageUuids.length > 0) { + // The plural list is this result's complete correlation; queue order must + // not join anyone it did not name. + return null + } + + const exact = session.dispatchWaiters.find((candidate) => candidate.sentUuid === uuid) + if (exact) { + const foldReceipt = isUserReplay && claudeReplayIsFoldReceipt(session, exact, uuid) + settleWaiter(session, exact, uuid, onSettledLate) + return isUserReplay && !foldReceipt ? { requestedAt: exact.requestedAt } : null + } + const retired = session.retiredDispatchWaiters.find((candidate) => candidate.sentUuid === uuid) + if (retired) { + forgetRetiredWaiter(session, retired) + recoverLateIdentity(session, retired, uuid, isUserReplay, onSettledLate) + return null + } + + if (isUserReplay) { + // Compatibility CLIs may mint a new replay uuid instead of echoing the + // client uuid. Content is an acceptable join only when it is the sole + // candidate on one side of the timeout boundary; with active and retired + // candidates present, identical prompts are intentionally left unknown. + const replayContentKey = claudeDispatchContentKey(envelope.content) + if (!session.replayContentFallbackBlocked && session.retiredDispatchWaiters.length === 0) { + const compatible = session.dispatchWaiters.filter( + (candidate) => candidate.replayContentKey === replayContentKey + ) + if (compatible.length === 1) { + const [candidate] = compatible + settleWaiter(session, candidate!, uuid, onSettledLate) + return { requestedAt: candidate!.requestedAt } + } + } else if (!session.replayContentFallbackBlocked && session.dispatchWaiters.length === 0) { + const lateCompatible = session.retiredDispatchWaiters.filter( + (candidate) => candidate.replayContentKey === replayContentKey + ) + if (lateCompatible.length === 1) { + const [candidate] = lateCompatible + forgetRetiredWaiter(session, candidate!) + recoverLateIdentity(session, candidate!, uuid, true, onSettledLate) + return null + } + } + return null + } + const current = session.dispatchWaiters[0] + if (isCompletedCommand && !current?.acceptsResult) { + return null + } + // A legacy result has no dispatch correlation. Any retired waiter makes queue order ambiguous, + // even when the retired dispatch was an ordinary turn rather than a slash command. + if (isCompletedCommand && session.retiredDispatchWaiters.length > 0) { + return null + } + // Once an eviction occurred, a fresh result uuid cannot be joined to a waiter by queue order. + if (isCompletedCommand && session.replayContentFallbackBlocked) { + return null + } + const waiter = uuid ? session.dispatchWaiters.shift() : undefined + if (waiter && uuid) { + settleWaiter(session, waiter, uuid, onSettledLate) + return isUserReplay ? { requestedAt: waiter.requestedAt } : null + } + return null +} + +/** The provider's own cycle state decides a fold: the CLI folds a send into the + * request cycle that is running when the send arrives, and it replays a folded + * send mid-cycle with the client uuid ADOPTED (measured: fold-fresh/-resumed, + * two-steers, early-steer). So an adopted replay after the running cycle has + * done work is a delivery receipt, not a turn boundary; a cycle's first send is + * its opener. Adoption is the capability check, read off the frame itself: a + * CLI that mints fresh replay uuids never qualifies. A new cycle announces + * itself with a root init (per-turn, measured), so a lost result cannot leave a + * stale turn swallowing the next turn's replay. */ +function claudeReplayIsFoldReceipt( + session: ClaudeSession, + waiter: ClaudeDispatchWaiter, + replayUuid: string +): boolean { + return replayUuid === waiter.sentUuid && session.translator?.openTurnInLiveProviderCycle === true +} + +function claudeResultUserMessageUuids(message: Record): string[] { + const uuids = message.user_message_uuids + return Array.isArray(uuids) + ? uuids.filter((member): member is string => typeof member === 'string' && member.length > 0) + : [] +} + +function settleWaiter( + session: ClaudeSession, + waiter: ClaudeDispatchWaiter, + uuid: string, + onSettledLate?: ClaudeLateDispatchSettlement +): void { + const index = session.dispatchWaiters.indexOf(waiter) + if (index !== -1) { + session.dispatchWaiters.splice(index, 1) + } + waiter.settledUuid = uuid + waiter.resolve(uuid) + // Dispatch returned on admission, so the replay is what settles delivery. + if (waiter.clientMessageId) { + onSettledLate?.({ + clientMessageId: waiter.clientMessageId, + providerIdentity: { provider: 'claude', sessionId: session.providerSessionId, uuid } + }) + } +} + +function recoverLateIdentity( + session: ClaudeSession, + waiter: ClaudeDispatchWaiter, + uuid: string, + isUserReplay: boolean, + onSettledLate?: ClaudeLateDispatchSettlement +): void { + if (!isUserReplay && !waiter.acceptsResult) { + return + } + // The provider acted on this dispatch, so the send it came from is delivered. + // A retired replay settles delivery only; it cannot reopen a turn. + if (waiter.clientMessageId) { + onSettledLate?.({ + clientMessageId: waiter.clientMessageId, + providerIdentity: { provider: 'claude', sessionId: session.providerSessionId, uuid } + }) + } +} diff --git a/src/main/claude/claude-stream-json-connection.test.ts b/src/main/claude/claude-stream-json-connection.test.ts index 6d3f0d89f30..e3682f03306 100644 --- a/src/main/claude/claude-stream-json-connection.test.ts +++ b/src/main/claude/claude-stream-json-connection.test.ts @@ -107,6 +107,18 @@ async function open( return connection } +function launchedArgv(spec: ProcessSpec | undefined): string[] { + const supervised = spec?.env?.ORCA_PROVIDER_SUPERVISOR_SPEC + if (!supervised) { + return [spec?.program ?? '', ...(spec?.args ?? [])] + } + const launch: unknown = JSON.parse(Buffer.from(supervised, 'base64').toString()) + if (!launch || typeof launch !== 'object' || !('command' in launch) || !('args' in launch)) { + return [] + } + return [String(launch.command), ...(Array.isArray(launch.args) ? launch.args.map(String) : [])] +} + function childEnv(): Record { return (spawned.at(-1)?.env ?? {}) as Record } @@ -201,8 +213,9 @@ describe('Claude stream-json connection', () => { const report = await until(() => readReportSafely(scenario), 'the scripted CLI report') expect(report.argv[0]).toBe(FAKE_CLI) // The .mjs fixture makes the SDK run it under node; a real CLI path is the program - // itself. Either way the resolved path is what Orca's spawner is asked to execute. - expect([spawned.at(-1)?.program, ...(spawned.at(-1)?.args ?? [])]).toContain(FAKE_CLI) + // itself. Either way the resolved path is what Orca's spawner is asked to execute, + // through the POSIX supervisor's spec where there is one. + expect(launchedArgv(spawned.at(-1))).toContain(FAKE_CLI) expect(report.argv).toContain('--replay-user-messages') expect(report.argv).toContain(`--session-id=${SESSION_ID}`) }) @@ -647,37 +660,63 @@ describe('Claude stream-json connection', () => { 20_000 ) - it('settles a spawn error followed by close as processless and closes idempotently', async () => { - const scenario = scriptScenario([HOLD_OPEN]) - const missingCli = join(scenario.cwd, 'claude-that-does-not-exist') - let fault: Error | null = null - let exit: Error | null = null - const connection = await open( - { ...launchFor(scenario), pathToClaudeCodeExecutable: missingCli }, - { - onFault: (error) => { - fault = error - }, - onExit: (error) => { - exit = error + it.skipIf(process.platform === 'win32')( + 'reports a missing CLI under the supervisor as a root exit that names the spawn error', + async () => { + const scenario = scriptScenario([HOLD_OPEN]) + const missingCli = join(scenario.cwd, 'claude-that-does-not-exist') + let exit: Error | null = null + const connection = await open( + { ...launchFor(scenario), pathToClaudeCodeExecutable: missingCli }, + { + onExit: (error) => { + exit = error + } } - } - ) + ) - await until( - () => (connection.exitVerdict.root === 'processless' ? connection.exitVerdict : null), - 'the processless spawn settlement' - ) - expect(connection.pid).toBeUndefined() - expect(fault).toBeInstanceOf(Error) - expect(exit).toBeNull() - await expect(Promise.all([connection.close(), connection.close()])).resolves.toEqual([ - true, - true - ]) - await expect(connection.close()).resolves.toBe(true) - expect(connection.exitVerdict).toEqual({ root: 'processless', tree: 'exited' }) - }) + const reported = await until(() => exit, 'the supervised spawn failure') + // The supervisor spawned, so this is its exit; only its stderr can say why. + expect(reported.message).toMatch(/\(code 127\).*ENOENT/s) + // A first-hand root exit, which releases the lease like a processless start did. + expect(connection.exitVerdict.root).toBe('exited') + } + ) + + it.skipIf(process.platform !== 'win32')( + 'settles a spawn error followed by close as processless and closes idempotently', + async () => { + const scenario = scriptScenario([HOLD_OPEN]) + const missingCli = join(scenario.cwd, 'claude-that-does-not-exist') + let fault: Error | null = null + let exit: Error | null = null + const connection = await open( + { ...launchFor(scenario), pathToClaudeCodeExecutable: missingCli }, + { + onFault: (error) => { + fault = error + }, + onExit: (error) => { + exit = error + } + } + ) + + await until( + () => (connection.exitVerdict.root === 'processless' ? connection.exitVerdict : null), + 'the processless spawn settlement' + ) + expect(connection.pid).toBeUndefined() + expect(fault).toBeInstanceOf(Error) + expect(exit).toBeNull() + await expect(Promise.all([connection.close(), connection.close()])).resolves.toEqual([ + true, + true + ]) + await expect(connection.close()).resolves.toBe(true) + expect(connection.exitVerdict).toEqual({ root: 'processless', tree: 'exited' }) + } + ) it('does not treat a child error event as first-hand root exit proof', async () => { const scenario = scriptScenario([HOLD_OPEN]) diff --git a/src/main/claude/claude-stream-json-connection.ts b/src/main/claude/claude-stream-json-connection.ts index 486407c0137..6f67e04b36e 100644 --- a/src/main/claude/claude-stream-json-connection.ts +++ b/src/main/claude/claude-stream-json-connection.ts @@ -309,15 +309,16 @@ export async function openClaudeStreamJsonConnection( closePromise ??= (async () => { closing = true resumeReading() - // Arm the descendant proof before ending stdin. The SDK may exit the root - // immediately; a post-exit walk cannot recover descendants that reparented. + // Arm the descendant proof before the stop. The root may exit immediately; + // a post-exit walk cannot recover descendants that reparented. await (tree.refresh?.() ?? tree.capture()) inbox.end() const proven = await proveClaudeChildExit({ child, exitPromise, exited: rootSettled, - tree + tree, + supervised: spawner.supervised }) inbox.fail(new Error('claude stream-json connection closed')) if (!proven) { diff --git a/src/main/claude/claude-structured-control-actions.test.ts b/src/main/claude/claude-structured-control-actions.test.ts index 3eaf496f75a..7f197788b68 100644 --- a/src/main/claude/claude-structured-control-actions.test.ts +++ b/src/main/claude/claude-structured-control-actions.test.ts @@ -243,6 +243,7 @@ describe('answerClaudePrompt', () => { const resolvePrompt = vi.fn() session.translator = { handle: vi.fn(), + openTurnInLiveProviderCycle: false, journalPrompts: { cancel: vi.fn(() => ({ accepted: true as const })), resolve: resolvePrompt diff --git a/src/main/claude/claude-structured-control-actions.ts b/src/main/claude/claude-structured-control-actions.ts index 8c1a6462e9c..8ec1aa2e9c4 100644 --- a/src/main/claude/claude-structured-control-actions.ts +++ b/src/main/claude/claude-structured-control-actions.ts @@ -1,10 +1,8 @@ import type { PermissionResult } from '@anthropic-ai/claude-agent-sdk' import type { ClaudePromptClaim } from './claude-structured-prompt-replies' import { ClaudeControlRequestError } from './claude-stream-json-connection' -import { - settleCancelledClaudeDispatchWaiters, - type ClaudeLateDispatchSettlement -} from './claude-structured-dispatch' +import { settleCancelledClaudeDispatchWaiters } from './claude-structured-dispatch' +import type { ClaudeLateDispatchSettlement } from './claude-replay-turn-resolution' import type { ClaudeSession } from './claude-structured-session-state' const INTERRUPT_CANCEL_QUEUED_CAPABILITY = 'interrupt_cancel_queued_v1' diff --git a/src/main/claude/claude-structured-dispatch-admission.test.ts b/src/main/claude/claude-structured-dispatch-admission.test.ts index 75553addd3c..c4578ca38d8 100644 --- a/src/main/claude/claude-structured-dispatch-admission.test.ts +++ b/src/main/claude/claude-structured-dispatch-admission.test.ts @@ -2,7 +2,8 @@ // completes, and nothing about elapsed time ever puts a message in doubt. import { describe, expect, it, vi } from 'vitest' -import { dispatchClaudeTurn, resolveClaudeReplayTurn } from './claude-structured-dispatch' +import { dispatchClaudeTurn } from './claude-structured-dispatch' +import { resolveClaudeReplayTurn } from './claude-replay-turn-resolution' import { childExited, sessionFor, @@ -61,8 +62,9 @@ describe('Claude structured dispatch admission', () => { true ) - // Queued while turn one is still running: Claude cannot echo it until that - // turn ends, so nothing about the wait is evidence of a delivery problem. + // Queued while turn one is still running: a fold is echoed mid-turn, a + // queued send only when its own turn starts — either way elapsed time is + // not evidence of a delivery problem. const queued = await dispatchClaudeTurn(session, { clientMessageId: 'client-2', body: userMessage([{ type: 'text', text: 'two' }]) diff --git a/src/main/claude/claude-structured-dispatch-waiters.ts b/src/main/claude/claude-structured-dispatch-waiters.ts index f3a3501ec8a..92b6a5048b2 100644 --- a/src/main/claude/claude-structured-dispatch-waiters.ts +++ b/src/main/claude/claude-structured-dispatch-waiters.ts @@ -10,11 +10,11 @@ export function forgetRetiredWaiter(session: ClaudeSession, waiter: ClaudeDispat } /** - * A waiter with no deadline. The echo Claude sends is emitted when the provider - * STARTS the turn, so a message queued behind a running turn cannot be echoed - * until that turn ends — an interval bounded only by the previous turn. Elapsed - * time is therefore not evidence about delivery, and nothing here expires. - * Waiters are retired by process facts instead: a failed write, or child exit. + * A waiter with no deadline. A mid-turn send Claude folds into the running turn + * is replayed mid-turn; one it runs later is replayed only when its own turn + * starts — an interval bounded only by the previous turn. Elapsed time is + * therefore not evidence about delivery, and nothing here expires. Waiters are + * retired by process facts instead: a failed write, or child exit. */ export function waitForReplay( session: ClaudeSession, diff --git a/src/main/claude/claude-structured-dispatch.test.ts b/src/main/claude/claude-structured-dispatch.test.ts index c540167de4c..92ae0253711 100644 --- a/src/main/claude/claude-structured-dispatch.test.ts +++ b/src/main/claude/claude-structured-dispatch.test.ts @@ -2,7 +2,8 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' -import { dispatchClaudeTurn, resolveClaudeReplayTurn } from './claude-structured-dispatch' +import { dispatchClaudeTurn } from './claude-structured-dispatch' +import { resolveClaudeReplayTurn } from './claude-replay-turn-resolution' import { ClaudeDispatchContentError, readClaudeImage } from './claude-structured-dispatch-content' import { claudeUnwrittenUserMessageError } from './claude-agent-sdk-user-message-queue' import type { ClaudeSession } from './claude-structured-session-state' diff --git a/src/main/claude/claude-structured-dispatch.ts b/src/main/claude/claude-structured-dispatch.ts index 220542fab91..612f6391ced 100644 --- a/src/main/claude/claude-structured-dispatch.ts +++ b/src/main/claude/claude-structured-dispatch.ts @@ -7,16 +7,8 @@ import { } from './claude-structured-dispatch-waiters' import type { AgentJournalMessageItem } from '../../shared/agent-session-journal-types' import type { AgentSessionDispatchOutcome } from '../native-chat/agent-session-wire/structured-agent-session-adapter' -import { - claudeHasReplayContent, - readClaudeMessageEnvelope -} from './claude-structured-item-translation' -import type { - ClaudeDispatchWaiter, - ClaudeLateDispatchOutcome, - ClaudeSession -} from './claude-structured-session-state' -import { readClaudeFrameString } from './claude-structured-init-proof' +import type { ClaudeSession } from './claude-structured-session-state' +import type { ClaudeLateDispatchSettlement } from './claude-replay-turn-resolution' import { claudeDispatchContentKey, claudeDispatchContentRejection, @@ -40,158 +32,6 @@ import { const MAX_ACTIVE_DISPATCH_WAITERS = 64 -/** Settles a provider-proven late outcome; replay rows independently reconcile acceptance. */ -export type ClaudeLateDispatchSettlement = (input: ClaudeLateDispatchOutcome) => void - -export type ClaudeReplayTurnOrigin = { requestedAt: number | null } - -export function resolveClaudeReplayTurn( - session: ClaudeSession, - message: Record, - onSettledLate?: ClaudeLateDispatchSettlement -): ClaudeReplayTurnOrigin | null { - const envelope = readClaudeMessageEnvelope(message) - const isUserReplay = - envelope?.role === 'user' && - message.parent_tool_use_id === null && - claudeHasReplayContent(envelope) - const isCompletedCommand = message.type === 'result' - if ( - (!isUserReplay && !isCompletedCommand) || - readClaudeFrameString(message, 'session_id') !== session.providerSessionId - ) { - return null - } - const uuid = readClaudeFrameString(message, 'uuid') - if (!uuid) { - return null - } - - // Newer SDK frames carry the client uuid that caused a turn. A correlation - // value is authoritative: never fall back to queue order or content, since - // identical prompts may be in flight across a timeout boundary. - const userMessageUuid = readClaudeFrameString(message, 'user_message_uuid') - if (userMessageUuid) { - const exact = session.dispatchWaiters.find( - (candidate) => candidate.sentUuid === userMessageUuid - ) - if (exact) { - settleWaiter(session, exact, uuid, onSettledLate) - return isUserReplay ? { requestedAt: exact.requestedAt } : null - } - const retired = session.retiredDispatchWaiters.find( - (candidate) => candidate.sentUuid === userMessageUuid - ) - if (retired) { - forgetRetiredWaiter(session, retired) - recoverLateIdentity(session, retired, uuid, isUserReplay, onSettledLate) - return null - } - return null - } - - const exact = session.dispatchWaiters.find((candidate) => candidate.sentUuid === uuid) - if (exact) { - settleWaiter(session, exact, uuid, onSettledLate) - return isUserReplay ? { requestedAt: exact.requestedAt } : null - } - const retired = session.retiredDispatchWaiters.find((candidate) => candidate.sentUuid === uuid) - if (retired) { - forgetRetiredWaiter(session, retired) - recoverLateIdentity(session, retired, uuid, isUserReplay, onSettledLate) - return null - } - - if (isUserReplay) { - // Compatibility CLIs may mint a new replay uuid instead of echoing the - // client uuid. Content is an acceptable join only when it is the sole - // candidate on one side of the timeout boundary; with active and retired - // candidates present, identical prompts are intentionally left unknown. - const replayContentKey = claudeDispatchContentKey(envelope.content) - if (!session.replayContentFallbackBlocked && session.retiredDispatchWaiters.length === 0) { - const compatible = session.dispatchWaiters.filter( - (candidate) => candidate.replayContentKey === replayContentKey - ) - if (compatible.length === 1) { - const [candidate] = compatible - settleWaiter(session, candidate!, uuid, onSettledLate) - return { requestedAt: candidate!.requestedAt } - } - } else if (!session.replayContentFallbackBlocked && session.dispatchWaiters.length === 0) { - const lateCompatible = session.retiredDispatchWaiters.filter( - (candidate) => candidate.replayContentKey === replayContentKey - ) - if (lateCompatible.length === 1) { - const [candidate] = lateCompatible - forgetRetiredWaiter(session, candidate!) - recoverLateIdentity(session, candidate!, uuid, true, onSettledLate) - return null - } - } - return null - } - const current = session.dispatchWaiters[0] - if (isCompletedCommand && !current?.acceptsResult) { - return null - } - // A legacy result has no dispatch correlation. Any retired waiter makes queue order ambiguous, - // even when the retired dispatch was an ordinary turn rather than a slash command. - if (isCompletedCommand && session.retiredDispatchWaiters.length > 0) { - return null - } - // Once an eviction occurred, a fresh result uuid cannot be joined to a waiter by queue order. - if (isCompletedCommand && session.replayContentFallbackBlocked) { - return null - } - const waiter = uuid ? session.dispatchWaiters.shift() : undefined - if (waiter && uuid) { - settleWaiter(session, waiter, uuid, onSettledLate) - return isUserReplay ? { requestedAt: waiter.requestedAt } : null - } - return null -} - -function settleWaiter( - session: ClaudeSession, - waiter: ClaudeDispatchWaiter, - uuid: string, - onSettledLate?: ClaudeLateDispatchSettlement -): void { - const index = session.dispatchWaiters.indexOf(waiter) - if (index !== -1) { - session.dispatchWaiters.splice(index, 1) - } - waiter.settledUuid = uuid - waiter.resolve(uuid) - // Dispatch returned on admission, so the replay is what settles delivery. - if (waiter.clientMessageId) { - onSettledLate?.({ - clientMessageId: waiter.clientMessageId, - providerIdentity: { provider: 'claude', sessionId: session.providerSessionId, uuid } - }) - } -} - -function recoverLateIdentity( - session: ClaudeSession, - waiter: ClaudeDispatchWaiter, - uuid: string, - isUserReplay: boolean, - onSettledLate?: ClaudeLateDispatchSettlement -): void { - if (!isUserReplay && !waiter.acceptsResult) { - return - } - // The provider acted on this dispatch, so the send it came from is delivered. - // A retired replay settles delivery only; it cannot reopen a turn. - if (waiter.clientMessageId) { - onSettledLate?.({ - clientMessageId: waiter.clientMessageId, - providerIdentity: { provider: 'claude', sessionId: session.providerSessionId, uuid } - }) - } -} - export function settleCancelledClaudeDispatchWaiters( session: ClaudeSession, cancelledUuids: readonly string[], @@ -327,6 +167,7 @@ export async function dispatchClaudeTurn( } // The write is the admission signal. Awaiting the echo here would block on the // turn already running, which is why the deadline this replaces kept declaring - // doubt about messages that were delivered. `settleWaiter` finishes the job. + // doubt about messages that were delivered. The replay resolution + // (`claude-replay-turn-resolution.ts`) finishes the job. return { state: 'admitted' } } diff --git a/src/main/claude/claude-structured-effort-reporting.test.ts b/src/main/claude/claude-structured-effort-reporting.test.ts index 7cf59ee555b..2c319c86ee7 100644 --- a/src/main/claude/claude-structured-effort-reporting.test.ts +++ b/src/main/claude/claude-structured-effort-reporting.test.ts @@ -7,7 +7,7 @@ import { import { readClaudeSettingsEffort } from './claude-structured-session-options' import type { ClaudeSession } from './claude-structured-session-state' import type { ClaudeStructuredSessionEvent } from './claude-structured-session-adapter' -import { acquired, fakeClaude } from './claude-structured-session-test-support' +import { USER_MESSAGE, acquired, fakeClaude } from './claude-structured-session-test-support' /** Verbatim from Claude Code 2.1.258's get_settings response. */ const REAL_SETTINGS = { @@ -93,7 +93,14 @@ describe('Claude effort reporting', () => { it('keeps the init fixture free of an effort the real frame never sends', async () => { const events: ClaudeStructuredSessionEvent[] = [] - await acquired(fakeClaude(), {}, events) + const adapter = await acquired(fakeClaude(), {}, events) + // Live: init arrives when the first command starts a cycle, not at startup. + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'seed-cycle', + body: USER_MESSAGE, + fence: 7 + }) const init = events.flatMap((event) => event.type === 'message' && event.message.subtype === 'init' ? [event.message] : [] ) diff --git a/src/main/claude/claude-structured-journal-translation.ts b/src/main/claude/claude-structured-journal-translation.ts index 16c054945a1..082b0024061 100644 --- a/src/main/claude/claude-structured-journal-translation.ts +++ b/src/main/claude/claude-structured-journal-translation.ts @@ -184,6 +184,21 @@ export function createClaudeJournalTranslator( } if (event.type === 'message') { context.observe(event.message, event.observedAt ?? Date.now()) + // A root init is the CLI starting a new request cycle (measured per turn, + // per queued turn, per background wake, per /compact); a send replayed + // after that cycle's first root work was folded into it. Task frames are + // not cycle work: they arrive between cycles too. + if (isRootClaudeFrame(event.message)) { + if (event.message.type === 'system' && event.message.subtype === 'init') { + turn.observeProviderCycleStart() + } else if ( + event.startsTurn === true || + event.message.type === 'assistant' || + event.message.type === 'stream_event' + ) { + turn.observeProviderCycleWork() + } + } } if (event.type === 'message' && handleStream(event.message, event.observedAt ?? Date.now())) { return @@ -281,6 +296,9 @@ export function createClaudeJournalTranslator( get currentTurnId() { return turn.id }, + get openTurnInLiveProviderCycle() { + return turn.openedInLiveProviderCycle + }, flush: streamedText.flush, childToolOwner: childQueries.childToolOwner, childActivity: childQueries.childActivity, diff --git a/src/main/claude/claude-structured-model-confirmation.test.ts b/src/main/claude/claude-structured-model-confirmation.test.ts index 96d7e9daa39..f4034400cbf 100644 --- a/src/main/claude/claude-structured-model-confirmation.test.ts +++ b/src/main/claude/claude-structured-model-confirmation.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it } from 'vitest' import { setClaudeStructuredOption } from './claude-structured-options' import type { ClaudeSession } from './claude-structured-session-state' -import { PROVIDER_SESSION_ID, acquired, fakeClaude } from './claude-structured-session-test-support' +import { + PROVIDER_SESSION_ID, + USER_MESSAGE, + acquired, + fakeClaude +} from './claude-structured-session-test-support' /** Verbatim rows from Claude Code 2.1.258's list_models response. */ const CATALOG = [ @@ -147,6 +152,13 @@ describe('Claude model confirmation', () => { routes: { list_models: () => CATALOG } }) const adapter = await acquired(claude) + // The model is confirmed by a cycle's init frame, so start one. + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'seed-cycle', + body: USER_MESSAGE, + fence: 7 + }) await adapter.setOption({ sessionId: 'session-1', key: 'effort', value: 'high', fence: 7 }) await expect(adapter.readOptions({ sessionId: 'session-1', fence: 7 })).resolves.toMatchObject({ diff --git a/src/main/claude/claude-structured-prompt-ownership.test.ts b/src/main/claude/claude-structured-prompt-ownership.test.ts index 3c290fa5184..53d6f63623d 100644 --- a/src/main/claude/claude-structured-prompt-ownership.test.ts +++ b/src/main/claude/claude-structured-prompt-ownership.test.ts @@ -17,9 +17,9 @@ import { acquired, adapterFor, fakeClaude, - identityFor, - invokeCanUseTool + identityFor } from './claude-structured-session-test-support' +import { invokeCanUseTool } from './claude-can-use-tool-test-support' function deferred(): { promise: Promise; resolve: () => void } { let resolve = (): void => {} diff --git a/src/main/claude/claude-structured-prompt-ownership.ts b/src/main/claude/claude-structured-prompt-ownership.ts index 864a99a0b77..e1c1aa371bf 100644 --- a/src/main/claude/claude-structured-prompt-ownership.ts +++ b/src/main/claude/claude-structured-prompt-ownership.ts @@ -10,7 +10,7 @@ import { cancelClaudeTurn, supportsClaudeQueuedInterruptCancellation } from './claude-structured-control-actions' -import type { ClaudeLateDispatchSettlement } from './claude-structured-dispatch' +import type { ClaudeLateDispatchSettlement } from './claude-replay-turn-resolution' import { buildClaudePromptReply } from './claude-structured-prompt-replies' import type { ClaudeSession } from './claude-structured-session-state' import type { ClaudePendingPrompt } from './claude-prompt-registry' diff --git a/src/main/claude/claude-structured-real-cli-fold.test.ts b/src/main/claude/claude-structured-real-cli-fold.test.ts new file mode 100644 index 00000000000..5ce21d83f17 --- /dev/null +++ b/src/main/claude/claude-structured-real-cli-fold.test.ts @@ -0,0 +1,194 @@ +// The fold receipt against the real CLI: a message sent while a turn runs is +// folded by the CLI into the running turn, and Orca must keep ONE turn row — +// no interrupted marking, no second bar. Runs only where a signed-in Claude CLI +// exists, like the rest of the real-CLI suite. Live sessions prove the session +// from a SessionStart hook frame BEFORE system/init arrives, which is exactly +// the path the fixture harness cannot fake end to end. + +import { randomUUID } from 'node:crypto' +import { mkdtemp } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it, vi } from 'vitest' +import type { + AgentJournalItemBody, + AgentSessionJournalIdentity +} from '../../shared/agent-session-journal-types' +import { readAgentJournalTurn } from '../../shared/agent-session-turn-record' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { CLAUDE_STRUCTURED_BASE_OPTIONS } from './claude-structured-launch-resolution' +import { + ClaudeStructuredSessionAdapter, + type ClaudeStructuredSessionEvent +} from './claude-structured-session-adapter' +import { + realClaudeAuthenticated, + realClaudeAvailable, + realClaudeCommand, + realClaudeLaunchHome +} from './claude-real-cli-availability-test-support' + +const SESSION_ID = 'real-cli-fold' +// Pins the live proof order (SessionStart hook frame before system/init) and lets the +// Bash steps run unprompted, whatever the config dir under test configures. +const FOLD_SESSION_SETTINGS = JSON.stringify({ + hooks: { SessionStart: [{ hooks: [{ type: 'command', command: 'echo' }] }] }, + permissions: { allow: ['Bash(sleep:*)'] } +}) + +function identity(providerSessionId: string): AgentSessionJournalIdentity { + return { + sessionId: SESSION_ID, + workspaceId: 'real-cli-fold-workspace', + hostId: 'local', + agent: 'claude', + providerHandle: { kind: 'claude', sessionId: providerSessionId, leafUuid: null } + } +} + +async function until(check: () => boolean, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (check()) { + return true + } + await new Promise((resolve) => setTimeout(resolve, 200)) + } + return check() +} + +describe.skipIf(!realClaudeAvailable)('Claude structured real CLI fold', () => { + it.skipIf(!realClaudeAuthenticated)( + 'keeps one turn row when the CLI folds a mid-turn send', + async () => { + const providerSessionId = randomUUID() + const { claudeConfigDir, env } = realClaudeLaunchHome() + const cwd = await mkdtemp(join(tmpdir(), 'orca-real-fold-')) + const events: ClaudeStructuredSessionEvent[] = [] + const turnRows: NonNullable>[] = [] + const sink: StructuredAgentSessionEventSink = { + appendItem: (_identity, body: AgentJournalItemBody) => { + const turn = readAgentJournalTurn(body) + if (turn) { + turnRows.push(turn) + } + }, + appendTombstone: () => {}, + publish: () => {} + } + const settled = vi.fn() + const adapter = new ClaudeStructuredSessionAdapter({ + resolveLaunch: async () => ({ + pathToClaudeCodeExecutable: realClaudeCommand, + options: { + ...CLAUDE_STRUCTURED_BASE_OPTIONS, + extraArgs: { + ...CLAUDE_STRUCTURED_BASE_OPTIONS.extraArgs, + settings: FOLD_SESSION_SETTINGS + }, + sessionId: providerSessionId + }, + cwd, + env, + claudeConfigDir, + providerSessionId, + resumeLeafUuid: null, + resumesTranscript: false, + continuesChain: false + }), + onEvent: (event) => events.push(event), + onDispatchSettledLate: settled, + readProcessStartTime: async () => 1 + }) + try { + await adapter.acquire({ + identity: identity(providerSessionId), + fence: 1, + spawnToken: 'real-cli-fold', + events: sink + }) + await adapter.awaitStarted(SESSION_ID) + // Startup proved from the SessionStart hook frame, with no init yet. + expect( + events.some( + (event) => + event.type === 'message' && + event.message.type === 'system' && + event.message.subtype === 'init' + ) + ).toBe(false) + + await expect( + adapter.dispatch({ + sessionId: SESSION_ID, + clientMessageId: 'client-A', + body: { + kind: 'message', + role: 'user', + blocks: [ + { + type: 'text', + text: 'Use the Bash tool to run `sleep 8` two separate times, one call at a time, waiting for each. Then reply exactly: FIRST DONE' + } + ] + }, + requestedAt: Date.now(), + fence: 1 + }) + ).resolves.toEqual({ state: 'admitted' }) + + // Wait for A's replay to open the turn, then send B mid-turn: the first + // `sleep 8` guarantees the CLI is still inside A's request cycle. + expect( + await until( + () => events.some((event) => event.type === 'message' && event.startsTurn === true), + 30_000 + ) + ).toBe(true) + await new Promise((resolve) => setTimeout(resolve, 2_000)) + await expect( + adapter.dispatch({ + sessionId: SESSION_ID, + clientMessageId: 'client-B', + body: { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: 'Also say the word banana at the end of your reply.' }] + }, + requestedAt: Date.now(), + fence: 1 + }) + ).resolves.toEqual({ state: 'admitted' }) + + const results = () => + events.flatMap((event) => + event.type === 'message' && event.message.type === 'result' ? [event.message] : [] + ) + expect(await until(() => results().length > 0, 90_000)).toBe(true) + + // The CLI folded: one result names both sends. (If this ever reports a + // lone send, the steer raced past the fold window — a miss, not a fold.) + const named = results().flatMap((message) => + Array.isArray(message.user_message_uuids) ? [message.user_message_uuids] : [] + ) + expect(named[0]).toHaveLength(2) + + // ONE turn for the whole run: never interrupted, and B settled accepted + // into it under its own adopted uuid. + expect(turnRows.every((turn) => turn.state !== 'interrupted')).toBe(true) + expect([...new Set(turnRows.map((turn) => turn.turnId))]).toHaveLength(1) + expect(turnRows.at(-1)).toMatchObject({ state: 'completed' }) + expect(settled).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: SESSION_ID, + clientMessageId: 'client-B', + providerIdentity: expect.objectContaining({ provider: 'claude' }) + }) + ) + } finally { + await adapter.closeAll() + } + }, + 150_000 + ) +}) diff --git a/src/main/claude/claude-structured-real-cli.test.ts b/src/main/claude/claude-structured-real-cli.test.ts index cfd4c8c33ea..20670147ca0 100644 --- a/src/main/claude/claude-structured-real-cli.test.ts +++ b/src/main/claude/claude-structured-real-cli.test.ts @@ -1,50 +1,24 @@ -import { spawnSync } from 'node:child_process' import { randomUUID } from 'node:crypto' import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { homedir, tmpdir } from 'node:os' import { basename, join, relative } from 'node:path' import { describe, expect, it } from 'vitest' import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' -import { resolveClaudeCommand } from '../codex-cli/command' import { resolveSessionFilePath } from '../native-chat/session-file-resolver' -import { getSpawnArgsForWindows } from '../win32-utils' import { CLAUDE_STRUCTURED_BASE_OPTIONS } from './claude-structured-launch-resolution' +import { + realClaudeAuthenticated, + realClaudeAuthStatus, + realClaudeAvailable, + realClaudeCommand +} from './claude-real-cli-availability-test-support' import { ClaudeStructuredSessionAdapter, type ClaudeStructuredSessionEvent } from './claude-structured-session-adapter' import type { ClaudeStructuredSessionAdapterDeps } from './claude-structured-session-state' -const command = resolveClaudeCommand() -const versionLaunch = getSpawnArgsForWindows(command, ['--version']) -const realClaudeAvailable = - spawnSync(versionLaunch.spawnCmd, versionLaunch.spawnArgs, { - stdio: 'ignore', - windowsHide: true, - timeout: 5_000 - }).status === 0 -const authStatusLaunch = getSpawnArgsForWindows(command, ['auth', 'status', '--json']) -/** The CLI's own account report — the only source of truth for where it writes that - * is not derived from Orca's own path expressions. */ -const realClaudeAuthStatus = (() => { - if (!realClaudeAvailable) { - return null - } - const result = spawnSync(authStatusLaunch.spawnCmd, authStatusLaunch.spawnArgs, { - encoding: 'utf8', - windowsHide: true, - timeout: 5_000 - }) - if (result.status !== 0) { - return null - } - try { - return JSON.parse(result.stdout) as { loggedIn?: boolean; projectsDirectory?: string } - } catch { - return null - } -})() -const realClaudeAuthenticated = realClaudeAuthStatus?.loggedIn === true +const command = realClaudeCommand function realAdapter( providerSessionId: string, diff --git a/src/main/claude/claude-structured-requested-stop.test.ts b/src/main/claude/claude-structured-requested-stop.test.ts new file mode 100644 index 00000000000..993109f4a37 --- /dev/null +++ b/src/main/claude/claude-structured-requested-stop.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest' +import type { + AgentJournalItemBody, + AgentJournalItemIdentity +} from '../../shared/agent-session-journal-types' +import { readAgentJournalTurn } from '../../shared/agent-session-turn-record' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import type { ClaudeStructuredSessionEvent } from './claude-structured-session-state' +import { + PROVIDER_SESSION_ID, + USER_MESSAGE, + adapterFor, + fakeClaude, + identityFor +} from './claude-structured-session-test-support' + +function journalSink() { + const items: { identity: AgentJournalItemIdentity; body: AgentJournalItemBody }[] = [] + const sink: StructuredAgentSessionEventSink = { + appendItem: (identity, body) => items.push({ identity, body }), + appendTombstone: () => {}, + publish: () => {} + } + return { sink, items } +} + +function frame(type: 'assistant' | 'user', uuid: string, content: unknown[]) { + return { + type, + uuid, + session_id: PROVIDER_SESSION_ID, + parent_tool_use_id: null, + message: { role: type, content } + } +} + +describe('a requested stop of a structured Claude chat', () => { + it('reads interrupted, not failed or crashed, through the frames the stop makes Claude emit', async () => { + const claude = fakeClaude({ replayUuid: 'turn-1' }) + const events: ClaudeStructuredSessionEvent[] = [] + const adapter = adapterFor(claude, {}, events) + const journal = journalSink() + await adapter.acquire({ + identity: identityFor(), + fence: 7, + spawnToken: 'spawn-9', + events: journal.sink + }) + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'client-1', + body: USER_MESSAGE, + fence: 7 + }) + const connection = claude.connections[0]! + connection.handlers.onMessage?.( + frame('assistant', 'assistant-1', [ + { type: 'tool_use', id: 'tool-1', name: 'Bash', input: { command: 'sleep 120' } } + ]) + ) + // What Claude 2.1.283 emitted within 35 ms of the supervised SIGTERM mid-tool, recorded + // against the real CLI: the killed tool's result, a status frame, and no `result` frame. + connection.close = async () => { + connection.handlers.onMessage?.( + frame('user', 'user-2', [ + { type: 'tool_result', tool_use_id: 'tool-1', is_error: true, content: 'Exit code 137' } + ]) + ) + connection.handlers.onMessage?.({ + type: 'system', + subtype: 'status', + uuid: 'status-1', + session_id: PROVIDER_SESSION_ID + }) + connection.closed = true + return true + } + + await expect(adapter.closeSession('session-1')).resolves.toBe(true) + + const turns = journal.items.flatMap((item) => { + const turn = readAgentJournalTurn(item.body) + return turn ? [turn] : [] + }) + expect(turns.at(-1)).toMatchObject({ turnId: 'turn-1', state: 'interrupted' }) + const ended = events.filter((event) => event.type === 'ended') + expect(ended).toEqual([expect.objectContaining({ reason: 'claude session closed' })]) + expect(ended[0]).not.toHaveProperty('cause') + }) +}) diff --git a/src/main/claude/claude-structured-session-acquisition.ts b/src/main/claude/claude-structured-session-acquisition.ts index ac8d65ba107..b5a7a5d3061 100644 --- a/src/main/claude/claude-structured-session-acquisition.ts +++ b/src/main/claude/claude-structured-session-acquisition.ts @@ -7,7 +7,7 @@ import { CLAUDE_AUTH_SWITCH_IN_PROGRESS_MESSAGE } from '../claude-accounts/envir import { isClaudeAuthSwitchInProgress } from '../claude-accounts/live-pty-gate' import { openClaudeStreamJsonConnection } from './claude-stream-json-connection' import { buildClaudePermissionCallbacks } from './claude-structured-inbound-control' -import { resolveClaudeReplayTurn } from './claude-structured-dispatch' +import { resolveClaudeReplayTurn } from './claude-replay-turn-resolution' import { readClaudeCapabilities, readClaudeFrameString, diff --git a/src/main/claude/claude-structured-session-adapter-turns.test.ts b/src/main/claude/claude-structured-session-adapter-turns.test.ts index c77d5ee0a91..d410861464a 100644 --- a/src/main/claude/claude-structured-session-adapter-turns.test.ts +++ b/src/main/claude/claude-structured-session-adapter-turns.test.ts @@ -223,6 +223,13 @@ describe('ClaudeStructuredSessionAdapter turns and controls', () => { } }) const adapter = await acquired(claude) + // The model is reported by a cycle's init frame, so start one. + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'seed-cycle', + body: USER_MESSAGE, + fence: 7 + }) await expect(adapter.readOptions({ sessionId: 'session-1', fence: 7 })).resolves.toEqual({ models: [ @@ -251,6 +258,13 @@ describe('ClaudeStructuredSessionAdapter turns and controls', () => { } }) const adapter = await acquired(claude) + // The custom model only reports on the first cycle's init frame. + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'seed-cycle', + body: USER_MESSAGE, + fence: 7 + }) const result = await adapter.readOptions({ sessionId: 'session-1', fence: 7 }) expect(result.models.map((model) => model.id)).toEqual([ diff --git a/src/main/claude/claude-structured-session-adapter.test.ts b/src/main/claude/claude-structured-session-adapter.test.ts index 376cac9d52f..ee14b6b525a 100644 --- a/src/main/claude/claude-structured-session-adapter.test.ts +++ b/src/main/claude/claude-structured-session-adapter.test.ts @@ -18,12 +18,12 @@ import { adapterFor, fakeClaude, identityFor, - invokeCanUseTool, PROVIDER_SESSION_ID, tick, USER_MESSAGE, type FakeConnection } from './claude-structured-session-test-support' +import { invokeCanUseTool } from './claude-can-use-tool-test-support' describe('ClaudeStructuredSessionAdapter.acquire', () => { it('pins the account and proves init without treating the system-frame uuid as a chain leaf', async () => { @@ -62,7 +62,8 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => { mintedAtFence: 7, observedAt: 1_700_000_000_500 }) - expect(events[0]).toMatchObject({ type: 'message', message: { subtype: 'init' } }) + // Live proof order: the SessionStart hook frame arrives before any init. + expect(events[0]).toMatchObject({ type: 'message', message: { subtype: 'hook_started' } }) }) it('restores persisted model and effort before publishing a reacquired session', async () => { @@ -344,7 +345,10 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => { session_id: 'foreign-provider-session' }) await Promise.resolve() - expect(events.filter((event) => event.type === 'message')).toHaveLength(1) + // Startup hook proof + the first cycle's init are admitted; nothing foreign is. + expect( + events.flatMap((event) => (event.type === 'message' ? [event.message.subtype] : [])) + ).toEqual(['hook_started', 'hook_response', 'init']) expect(settled).not.toHaveBeenCalled() connection.handlers.onMessage?.({ diff --git a/src/main/claude/claude-structured-session-recovery.test.ts b/src/main/claude/claude-structured-session-recovery.test.ts index a5647caee84..71a7ac9ce31 100644 --- a/src/main/claude/claude-structured-session-recovery.test.ts +++ b/src/main/claude/claude-structured-session-recovery.test.ts @@ -9,10 +9,10 @@ import { adapterFor, fakeClaude, identityFor, - invokeCanUseTool, PROVIDER_SESSION_ID, tick } from './claude-structured-session-test-support' +import { invokeCanUseTool } from './claude-can-use-tool-test-support' describe('ClaudeStructuredSessionAdapter close and exit recovery', () => { it('shares concurrent close finalization and emits lifecycle once', async () => { diff --git a/src/main/claude/claude-structured-session-state.ts b/src/main/claude/claude-structured-session-state.ts index a0b0f321679..6848d9a1943 100644 --- a/src/main/claude/claude-structured-session-state.ts +++ b/src/main/claude/claude-structured-session-state.ts @@ -42,7 +42,9 @@ export type ClaudeStructuredSessionEvent = type: 'message' sessionId: string message: Record - /** Present only when this replay acknowledged Orca's in-flight dispatch. */ + /** Present only when this replay acknowledged Orca's in-flight dispatch + * AND opens a turn; a replay folded into the running turn settles + * delivery without one. */ startsTurn?: true /** Submission instant of the dispatch this replay acknowledged; the origin * of the turn it opens. Absent when the host cannot name a send. */ diff --git a/src/main/claude/claude-structured-session-test-support.ts b/src/main/claude/claude-structured-session-test-support.ts index 51c7c1c8fff..0380d5ac12f 100644 --- a/src/main/claude/claude-structured-session-test-support.ts +++ b/src/main/claude/claude-structured-session-test-support.ts @@ -51,6 +51,9 @@ export function fakeClaude( initSessionId?: string initUuid?: string initModel?: string + /** 'session-start' (default) mirrors live: a SessionStart hook frame proves the + * session and system/init arrives only when the first command starts a cycle. + * 'init' emits init at startup — an UNMEASURED shape, opt-in only. */ initProof?: 'init' | 'session-start' | 'none' initAccount?: unknown initCommands?: unknown @@ -81,6 +84,22 @@ export function fakeClaude( return route ? route(params) : undefined } const openConnection: typeof openClaudeStreamJsonConnection = async (launch, handlers = {}) => { + let cycleInitEmitted = false + // Keys mirror the real system/init frame, which carries `model` but no + // effort of any kind: the current effort only comes back from get_settings. + // Never add a field the CLI does not send. + const emitCycleInit = (): void => { + cycleInitEmitted = true + handlers.onMessage?.({ + type: 'system', + subtype: 'init', + session_id: options.initSessionId ?? PROVIDER_SESSION_ID, + uuid: options.initUuid ?? 'init-uuid', + model: options.initModel ?? 'claude-sonnet-5', + apiKeySource: 'none', + ...(options.capabilities ? { capabilities: options.capabilities } : {}) + }) + } const connection: FakeConnection = { launch, handlers, @@ -102,7 +121,13 @@ export function fakeClaude( // The SDK rejects pending control requests once the transport ends. throw new Error('Query closed before response received') } - if (options.initProof === 'session-start') { + if (options.initProof === 'init') { + // UNMEASURED startup shape, kept only as an explicit opt-in: live + // sessions prove startup with a SessionStart hook frame instead. + emitCycleInit() + } else if (options.initProof !== 'none') { + // The live proof order (measured through Orca's adapter): SessionStart + // hook frames arrive first; system/init only when a cycle starts. handlers.onMessage?.({ type: 'system', subtype: 'hook_started', @@ -110,22 +135,19 @@ export function fakeClaude( session_id: options.initSessionId ?? PROVIDER_SESSION_ID, uuid: options.initUuid ?? 'init-uuid' }) - } else if (options.initProof !== 'none') { - // Keys mirror the real system/init frame, which carries `model` but no - // effort of any kind: the current effort only comes back from - // get_settings. Never add a field the CLI does not send. handlers.onMessage?.({ type: 'system', - subtype: 'init', + subtype: 'hook_response', + hook_name: 'SessionStart:startup', session_id: options.initSessionId ?? PROVIDER_SESSION_ID, - uuid: options.initUuid ?? 'init-uuid', - model: options.initModel ?? 'claude-sonnet-5', - apiKeySource: 'none', - ...(options.capabilities ? { capabilities: options.capabilities } : {}) + uuid: 'hook-response-uuid' }) } return { models: options.initModels ?? [{ value: 'claude-sonnet', displayName: 'Sonnet' }], + // Capabilities ride the initialize result, where startup facts read + // them regardless of when the first init frame arrives. + ...(options.capabilities ? { capabilities: options.capabilities } : {}), ...(options.initCommands === undefined ? {} : { commands: options.initCommands }), ...(options.initAccount === undefined ? {} : { account: options.initAccount }) } @@ -184,6 +206,11 @@ export function fakeClaude( await beforeDispatch() } connection.sent.push(message) + // Live: the first command starts a request cycle, whose init precedes + // the replay. Later cycles are the test's own frames. + if (message.type === 'user' && !cycleInitEmitted && options.initProof !== 'none') { + emitCycleInit() + } if (message.type === 'user' && options.replayUuid !== null) { const configuredReplayUuid = options.replayUuids ? options.replayUuids[replayIndex++] @@ -297,29 +324,3 @@ export function recordingJournalSink(): StructuredAgentSessionEventSink { export function tick(): Promise { return new Promise((resolve) => setImmediate(resolve)) } - -export function invokeCanUseTool( - connection: FakeConnection, - toolName: string, - requestId: string, - toolUseID: string, - extra: { - input?: Record - suggestions?: unknown[] - signal?: AbortSignal - } = {} -): { promise: Promise; settled: () => boolean } { - const options = { - requestId, - toolUseID, - signal: extra.signal ?? new AbortController().signal, - ...(extra.suggestions ? { suggestions: extra.suggestions } : {}) - } as unknown as Parameters>[2] - let done = false - const promise = Promise.resolve( - connection.handlers.canUseTool?.(toolName, extra.input ?? {}, options) - ).finally(() => { - done = true - }) - return { promise, settled: () => done } -} diff --git a/src/main/claude/claude-supervised-stop-real-cli.test.ts b/src/main/claude/claude-supervised-stop-real-cli.test.ts new file mode 100644 index 00000000000..82432e3fd6e --- /dev/null +++ b/src/main/claude/claude-supervised-stop-real-cli.test.ts @@ -0,0 +1,244 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { homedir, tmpdir } from 'node:os' +import { join } from 'node:path' +import { randomUUID } from 'node:crypto' +import { afterEach, describe, expect, it } from 'vitest' +import { runProcess } from '../../shared/child-process/run-process' +import { resolveSessionFilePath } from '../native-chat/session-file-resolver' +import { SUPERVISED_GRACEFUL_EXIT_MS } from './claude-child-exit-proof-ladder' +import { + openClaudeStreamJsonConnection, + type ClaudeStreamJsonConnection +} from './claude-stream-json-connection' +import { + CLAUDE_STRUCTURED_BASE_OPTIONS, + claudeStructuredPermissionOptions +} from './claude-structured-launch-resolution' + +// Opt-in only: spends a real (haiku) turn per case. Run it in an isolated HOME with +// ORCA_REAL_CLAUDE_BIN set, and ORCA_REAL_CLAUDE_SETTINGS when auth lives in a settings file. +const CLAUDE_BIN = process.env.ORCA_REAL_CLAUDE_BIN ?? '' +const enabled = + process.env.ORCA_REAL_CLAUDE_SUPERVISED_STOP === '1' && + process.platform !== 'win32' && + CLAUDE_BIN.length > 0 +const FRAMES_OUT = process.env.ORCA_REAL_CLAUDE_FRAMES_OUT +const TOOL_MARKER = 'orca_supervised_stop_probe' +const TOOL_PROMPT = `Use the Bash tool to run exactly this command, with no timeout argument, and nothing else: python3 -c "import time; time.sleep(120) # ${TOOL_MARKER}"` + +type Frame = { at: number; message: Record } +type Row = { pid: number; ppid: number; command: string } + +const recordedPids = new Set() +const tempDirs: string[] = [] +const connections: ClaudeStreamJsonConnection[] = [] + +function alive(pid: number): boolean { + try { + process.kill(pid, 0) + return true + } catch (error) { + return !(error instanceof Error && 'code' in error && error.code === 'ESRCH') + } +} + +async function processTable(): Promise { + const result = await runProcess({ program: 'ps', args: ['-axo', 'pid=,ppid=,command='] }) + return result.stdout.split('\n').flatMap((line) => { + const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line) + return match ? [{ pid: Number(match[1]), ppid: Number(match[2]), command: match[3] }] : [] + }) +} + +async function descendantsOf(rootPid: number): Promise { + const rows = await processTable() + const found: Row[] = [] + const frontier = [rootPid] + while (frontier.length > 0) { + const parent = frontier.pop() + for (const row of rows.filter((candidate) => candidate.ppid === parent)) { + found.push(row) + frontier.push(row.pid) + } + } + return found +} + +async function until(read: () => Promise | T | null, what: string, ms = 90_000) { + const deadline = Date.now() + ms + for (;;) { + const value = await read() + if (value !== null) { + return value + } + if (Date.now() >= deadline) { + throw new Error(`timed out waiting for ${what}`) + } + await new Promise((resolve) => setTimeout(resolve, 250)) + } +} + +async function open(sessionId: string, cwd: string, resume: boolean, frames: Frame[]) { + const settings = process.env.ORCA_REAL_CLAUDE_SETTINGS + const permission = claudeStructuredPermissionOptions('bypassPermissions') + const connection = await openClaudeStreamJsonConnection( + { + pathToClaudeCodeExecutable: CLAUDE_BIN, + options: { + ...CLAUDE_STRUCTURED_BASE_OPTIONS, + model: 'haiku', + extraArgs: { + ...CLAUDE_STRUCTURED_BASE_OPTIONS.extraArgs, + ...permission.extraArgs, + ...(settings ? { settings } : {}) + }, + ...(resume ? { resume: sessionId } : { sessionId }) + }, + cwd + }, + { onMessage: (message) => frames.push({ at: Date.now(), message }) } + ) + connections.push(connection) + recordedPids.add(connection.pid!) + return connection +} + +function userMessage(text: string): Record { + return { + type: 'user', + message: { role: 'user', content: [{ type: 'text', text }] }, + parent_tool_use_id: null, + session_id: '' + } +} + +async function transcriptLines(sessionId: string): Promise { + const configDir = process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), '.claude') + const path = await until( + () => + resolveSessionFilePath('claude', sessionId, { + claudeProjectsDir: join(configDir, 'projects') + }), + 'the transcript', + 15_000 + ) + return readFileSync(path, 'utf8') +} + +function expectLineAtomic(contents: string): void { + expect(contents.length).toBeGreaterThan(0) + expect(contents.endsWith('\n')).toBe(true) + for (const line of contents.split('\n').filter((entry) => entry.trim())) { + expect(() => JSON.parse(line)).not.toThrow() + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function summarize(frames: Frame[], since: number): string[] { + return frames + .filter((frame) => frame.at >= since) + .map(({ at, message }) => { + const inner = isRecord(message.message) ? message.message.content : undefined + const blocks = (Array.isArray(inner) ? inner : []) + .filter(isRecord) + .map((block) => + block.type === 'tool_result' + ? `tool_result(is_error=${String(block.is_error)}:${JSON.stringify(block.content).slice(0, 80)})` + : String(block.type) + ) + return `+${at - since}ms ${String(message.type)}/${String(message.subtype ?? '')}${ + message.type === 'result' ? `(is_error=${String(message.is_error)})` : '' + } ${blocks.join(',')}`.trim() + }) +} + +async function resumeAndAsk(sessionId: string, cwd: string): Promise { + const frames: Frame[] = [] + const resumed = await open(sessionId, cwd, true, frames) + await resumed.send(userMessage('In one short line: what command did I ask you to run?')) + const result = await until( + () => frames.find((frame) => frame.message.type === 'result')?.message ?? null, + 'the resumed turn result' + ) + expect(result.subtype).toBe('success') + await expect(resumed.close()).resolves.toBe(true) +} + +type StopCase = 'close mid-tool' | 'SIGTERM to the supervisor mid-tool' | 'close while idle' + +afterEach(async () => { + for (const pid of recordedPids) { + if (alive(pid)) { + process.kill(pid, 'SIGKILL') + } + } + recordedPids.clear() + connections.splice(0) + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }) + } +}) + +describe.runIf(enabled)('real Claude stopped through the POSIX supervisor', () => { + it.each(['close mid-tool', 'SIGTERM to the supervisor mid-tool', 'close while idle'])( + '%s: stops Claude and its tool, keeps the transcript line-atomic, and resumes', + async (stopCase) => { + const cwd = mkdtempSync(join(tmpdir(), 'orca-real-claude-stop-')) + tempDirs.push(cwd) + const sessionId = randomUUID() + const frames: Frame[] = [] + const connection = await open(sessionId, cwd, false, frames) + const supervisor = connection.pid! + const midTool = stopCase !== 'close while idle' + await connection.send(userMessage(midTool ? TOOL_PROMPT : 'Reply with the single word: ok')) + const descendants = midTool + ? await until(async () => { + const rows = await descendantsOf(supervisor) + return rows.some((row) => row.command.includes(TOOL_MARKER)) ? rows : null + }, 'the Bash tool').catch((error: unknown) => { + console.log('[no tool] frames so far:', summarize(frames, 0)) + throw error + }) + : await until( + async () => + frames.some((frame) => frame.message.type === 'result') + ? await descendantsOf(supervisor) + : null, + 'the idle turn' + ) + for (const row of descendants) { + recordedPids.add(row.pid) + } + + const signalledAt = Date.now() + if (stopCase === 'SIGTERM to the supervisor mid-tool') { + process.kill(supervisor, 'SIGTERM') + } else { + await expect(connection.close()).resolves.toBe(true) + } + const gone = await until( + () => ([supervisor, ...descendants.map((row) => row.pid)].some(alive) ? null : true), + 'the supervisor, Claude and its tools to exit', + SUPERVISED_GRACEFUL_EXIT_MS + 2_000 + ) + const stoppedMs = Date.now() - signalledAt + const after = summarize(frames, signalledAt) + console.log(`[${stopCase}] stopped in ${stoppedMs} ms; frames after the stop:`, after) + if (FRAMES_OUT) { + writeFileSync( + `${FRAMES_OUT}.${stopCase.replaceAll(' ', '-')}.json`, + JSON.stringify({ stoppedMs, after }, null, 2) + ) + } + expect(gone).toBe(true) + + expectLineAtomic(await transcriptLines(sessionId)) + await resumeAndAsk(sessionId, cwd) + expectLineAtomic(await transcriptLines(sessionId)) + }, + 180_000 + ) +}) diff --git a/src/main/claude/claude-supervised-stop.integration.test.ts b/src/main/claude/claude-supervised-stop.integration.test.ts new file mode 100644 index 00000000000..d0a5f9ea0cc --- /dev/null +++ b/src/main/claude/claude-supervised-stop.integration.test.ts @@ -0,0 +1,225 @@ +import { spawn, type ChildProcess } from 'node:child_process' +import { existsSync, mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import type { SpawnOptions as SdkSpawnOptions } from '@anthropic-ai/claude-agent-sdk' +import { spawnProcess } from '../../shared/child-process/run-process' +import type { ProcessSpec } from '../../shared/child-process/process-spec' +import { + PROVIDER_SIGTERM_GRACE_MS, + PROVIDER_STDIN_END_GRACE_MS, + PROVIDER_SUPERVISOR_MAX_STOP_MS +} from '../codex/codex-app-server-posix-supervisor' +import { proveClaudeChildExit } from './claude-agent-sdk-exit-proof' +import { createClaudeCodeProcessSpawn } from './claude-agent-sdk-process-spawn' + +// Stands in for Claude mid-turn: stdin end does not stop it, the way EOF lets the real CLI finish +// its turn. Its tool leads its own group, as the CLI's Bash tool shells do, and only Claude's own +// SIGTERM handler reaps it. The daemon double-forks out of the tree before anything looks. +const FAKE_CLAUDE = String.raw` + const { spawn } = require('node:child_process') + const { writeFileSync } = require('node:fs') + process.stdin.resume() + process.stdout.on('error', () => {}) + const tool = spawn(process.execPath, ['-e', 'setInterval(() => {}, 60000)'], { + detached: true, + stdio: 'ignore' + }) + const forker = spawn( + process.execPath, + ['-e', "const d = require('node:child_process').spawn(process.execPath, ['-e', 'setInterval(() => {}, 60000)'], { detached: true, stdio: 'ignore' }); d.unref(); process.stdout.write(String(d.pid))"], + { stdio: ['ignore', 'pipe', 'ignore'] } + ) + let daemon = '' + forker.stdout.on('data', (chunk) => { daemon += chunk }) + forker.once('exit', () => { + process.stdout.write(JSON.stringify({ claude: process.pid, tool: tool.pid, daemon: Number(daemon) }) + '\n') + }) + process.on('SIGTERM', () => { + if (process.env.ORCA_TEST_CLAUDE_IGNORES_SIGTERM) return + try { process.kill(-tool.pid, 'SIGKILL') } catch {} + writeFileSync(process.env.ORCA_TEST_SIGTERM_MARKER, 'reaped') + process.exit(143) + }) + setInterval(() => {}, 60000) +` + +// Stands in for Orca's main: starts exactly the spec Claude's spawner built, then can be SIGKILLed. +const OWNER = String.raw` + const { spawn } = require('node:child_process') + const spec = JSON.parse(process.env.ORCA_TEST_SPAWN_SPEC) + const env = { ...spec.env } + if (env.ORCA_PROVIDER_SUPERVISOR_SPEC) { + const supervisor = JSON.parse(Buffer.from(env.ORCA_PROVIDER_SUPERVISOR_SPEC, 'base64').toString()) + supervisor.ownerPid = process.pid + env.ORCA_PROVIDER_SUPERVISOR_SPEC = Buffer.from(JSON.stringify(supervisor)).toString('base64') + } + const child = spawn(spec.program, spec.args, { + cwd: spec.cwd, + env, + detached: spec.detached, + stdio: ['pipe', 'pipe', 'ignore'] + }) + process.stdout.write(JSON.stringify({ root: child.pid }) + '\n') + child.stdout.pipe(process.stdout) + setInterval(() => {}, 60000) +` + +const recordedPids = new Set() +const tempDirs: string[] = [] + +function alive(pid: number): boolean { + try { + process.kill(pid, 0) + return true + } catch (error) { + return !(error instanceof Error && 'code' in error && error.code === 'ESRCH') + } +} + +async function waitFor(predicate: () => boolean, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs + while (!predicate()) { + if (Date.now() >= deadline) { + return false + } + await new Promise((resolve) => setTimeout(resolve, 25)) + } + return true +} + +function readPids(child: ChildProcess, keys: readonly string[]): Promise> { + return new Promise((resolve, reject) => { + const pids: Record = {} + let buffered = '' + const timeout = setTimeout(() => reject(new Error(`no ${keys.join('/')} pids`)), 10_000) + const onData = (chunk: Buffer): void => { + buffered += chunk.toString() + const lines = buffered.split('\n') + buffered = lines.pop() ?? '' + for (const line of lines) { + const parsed: unknown = JSON.parse(line) + for (const [key, pid] of Object.entries(parsed ?? {})) { + if (typeof pid === 'number' && pid > 0) { + pids[key] = pid + recordedPids.add(pid) + } + } + } + if (keys.every((key) => key in pids)) { + clearTimeout(timeout) + child.stdout!.off('data', onData) + child.stdout!.resume() + resolve(pids) + } + } + child.stdout!.on('data', onData) + }) +} + +function sdkOptions(env: Record): SdkSpawnOptions { + const dir = mkdtempSync(join(tmpdir(), 'orca-claude-supervised-')) + tempDirs.push(dir) + return { + command: process.execPath, + args: ['-e', FAKE_CLAUDE], + cwd: dir, + env: { + ...process.env, + ORCA_TEST_SIGTERM_MARKER: join(dir, 'sigterm-reap'), + ...env + }, + signal: new AbortController().signal + } +} + +/** Claude spawned through Orca's own spawner and stopped by Orca's own close ladder. */ +async function spawnClaude(env: Record = {}) { + const spawner = createClaudeCodeProcessSpawn(spawnProcess) + const options = sdkOptions(env) + const child = spawner.spawn(options) + recordedPids.add(child.pid!) + let exited = false + const exit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => + child.once('exit', (code, signal) => { + exited = true + resolve({ code, signal }) + }) + ) + const pids = await readPids(child, ['claude', 'tool', 'daemon']) + const close = (): Promise => + proveClaudeChildExit({ + child, + exitPromise: exit.then(() => undefined), + exited: () => exited, + supervised: spawner.supervised + }) + return { child, exit, pids, close, marker: String(options.env.ORCA_TEST_SIGTERM_MARKER) } +} + +afterEach(() => { + for (const pid of recordedPids) { + if (alive(pid)) { + process.kill(pid, 'SIGKILL') + } + } + recordedPids.clear() + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }) + } +}) + +describe.runIf(process.platform !== 'win32')('Claude under the POSIX provider supervisor', () => { + it('stops a mid-turn Claude on close instead of letting stdin end finish its turn', async () => { + const { child, exit, pids, close, marker } = await spawnClaude() + expect(child.pid).not.toBe(pids.claude) + + const startedAt = Date.now() + await expect(close()).resolves.toBe(true) + + // Claude's own SIGTERM reap ran at once, not after the supervisor's stdin-end grace. + expect(Date.now() - startedAt).toBeLessThan(PROVIDER_STDIN_END_GRACE_MS) + expect(existsSync(marker)).toBe(true) + await expect(exit).resolves.toEqual({ code: null, signal: 'SIGTERM' }) + expect(alive(pids.claude)).toBe(false) + expect(alive(pids.tool)).toBe(false) + }) + + it('lets the supervisor escalate a Claude that ignores SIGTERM, and exits only after it', async () => { + const { exit, pids, close } = await spawnClaude({ ORCA_TEST_CLAUDE_IGNORES_SIGTERM: '1' }) + + const startedAt = Date.now() + await expect(close()).resolves.toBe(true) + + const elapsed = Date.now() - startedAt + expect(elapsed).toBeGreaterThanOrEqual(PROVIDER_SIGTERM_GRACE_MS) + expect(elapsed).toBeLessThan(PROVIDER_SUPERVISOR_MAX_STOP_MS + 1_000) + // The supervisor's own SIGTERM stop finished the job; nothing forced the supervisor itself. + await expect(exit).resolves.toEqual({ code: null, signal: 'SIGTERM' }) + expect(alive(pids.claude)).toBe(false) + }) + + it("stops Claude and its tool when Orca's main dies, and leaves a daemon that left its tree alone", async () => { + const specs: ProcessSpec[] = [] + createClaudeCodeProcessSpawn((spec) => { + specs.push(spec) + return spawnProcess({ program: 'true' }) + }).spawn(sdkOptions({})) + const owner = spawn(process.execPath, ['-e', OWNER], { + env: { ...process.env, ORCA_TEST_SPAWN_SPEC: JSON.stringify(specs[0]) }, + stdio: ['ignore', 'pipe', 'ignore'] + }) + recordedPids.add(owner.pid!) + const pids = await readPids(owner, ['root', 'claude', 'tool', 'daemon']) + + owner.kill('SIGKILL') + + expect(await waitFor(() => !alive(pids.claude), PROVIDER_SUPERVISOR_MAX_STOP_MS)).toBe(true) + expect(await waitFor(() => !alive(pids.root), PROVIDER_SUPERVISOR_MAX_STOP_MS)).toBe(true) + // In its own group, so only Claude's SIGTERM handler could have reaped it. + expect(alive(pids.tool)).toBe(false) + // Not the conversation's writer: nothing proves it orphaned, so the stop never reaches it. + expect(alive(pids.daemon)).toBe(true) + }) +}) diff --git a/src/main/claude/claude-turn-ownership.test.ts b/src/main/claude/claude-turn-ownership.test.ts index 338a6937716..81a84989aa4 100644 --- a/src/main/claude/claude-turn-ownership.test.ts +++ b/src/main/claude/claude-turn-ownership.test.ts @@ -96,6 +96,7 @@ function sessionHoldingTurn(turnId: string | null): ReturnType { session.dispatchSequence = 1 session.translator = { handle: vi.fn(), + openTurnInLiveProviderCycle: false, journalPrompts: { cancel: vi.fn(), resolve: vi.fn() }, currentTurnId: 'turn-1', flush: vi.fn(), diff --git a/src/main/claude/hook-settings.ts b/src/main/claude/hook-settings.ts index 6ce4ea4869c..381e4d26b6f 100644 --- a/src/main/claude/hook-settings.ts +++ b/src/main/claude/hook-settings.ts @@ -19,8 +19,8 @@ import { isGitBashAvailable } from '../git-bash' import type { ClaudeManagedHookPlan } from './claude-managed-hook-events' export type ClaudeCompatibleHookSettings = { - configDirName: '.claude' | '.openclaude' | '.qoder' - scriptBaseName: 'claude-hook' | 'openclaude-hook' | 'qoder-hook' + configDirName: '.claude' | '.openclaude' | '.qoder' | '.codebuddy' + scriptBaseName: 'claude-hook' | 'openclaude-hook' | 'qoder-hook' | 'codebuddy-hook' usesWindowsCompatLauncher: boolean windowsHookShell?: 'powershell' } diff --git a/src/main/codebuddy/hook-service.test.ts b/src/main/codebuddy/hook-service.test.ts new file mode 100644 index 00000000000..e20ae46e39c --- /dev/null +++ b/src/main/codebuddy/hook-service.test.ts @@ -0,0 +1,47 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import type * as OsModule from 'node:os' +import { join } from 'node:path' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +const sandbox = vi.hoisted(() => ({ home: '' })) +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + homedir: () => sandbox.home +})) +vi.mock('electron', () => ({ app: { getPath: () => sandbox.home } })) +import { codebuddyHookService, CODEBUDDY_HOOK_EVENTS } from './hook-service' + +beforeAll(() => { + sandbox.home = mkdtempSync(join(tmpdir(), 'orca-codebuddy-test-')) + mkdirSync(join(sandbox.home, '.codebuddy')) +}) +afterAll(() => rmSync(sandbox.home, { recursive: true, force: true })) + +describe('CodeBuddy managed hooks', () => { + it('preserves user configuration through repeated install and removal', () => { + const path = join(sandbox.home, '.codebuddy', 'settings.json') + const userHook = { hooks: [{ type: 'command', command: 'echo user-hook' }] } + const settings = { + model: 'custom', + hooks: { Stop: [userHook] }, + statusLine: { command: 'user-status' } + } + writeFileSync(path, JSON.stringify(settings)) + expect(codebuddyHookService.install().state).toBe('installed') + expect(codebuddyHookService.install().state).toBe('installed') + const installed = JSON.parse(readFileSync(path, 'utf8')) + for (const event of CODEBUDDY_HOOK_EVENTS) { + expect(installed.hooks[event]).toHaveLength(event === 'Stop' ? 2 : 1) + } + expect(installed.statusLine).toEqual(settings.statusLine) + expect(codebuddyHookService.remove().state).toBe('not_installed') + expect(JSON.parse(readFileSync(path, 'utf8'))).toMatchObject(settings) + }) + + it('leaves malformed user settings untouched', () => { + const path = join(sandbox.home, '.codebuddy', 'settings.json') + writeFileSync(path, '{broken') + expect(codebuddyHookService.install().state).toBe('error') + expect(readFileSync(path, 'utf8')).toBe('{broken') + }) +}) diff --git a/src/main/codebuddy/hook-service.ts b/src/main/codebuddy/hook-service.ts new file mode 100644 index 00000000000..1a7c9619a34 --- /dev/null +++ b/src/main/codebuddy/hook-service.ts @@ -0,0 +1,34 @@ +import type { ClaudeManagedHookPlan } from '../claude/claude-managed-hook-events' +import { ClaudeHookService } from '../claude/hook-service' + +export const CODEBUDDY_HOOK_EVENTS = [ + 'SessionStart', + 'SessionEnd', + 'UserPromptSubmit', + 'PreToolUse', + 'PostToolUse', + 'PostToolUseFailure', + 'PermissionRequest', + 'Stop', + 'StopFailure', + 'Notification' +] as const + +export const CODEBUDDY_MANAGED_HOOK_PLAN: ClaudeManagedHookPlan = { + install: CODEBUDDY_HOOK_EVENTS.map((eventName) => ({ eventName, definition: {} })), + retire: [], + statusLine: 'leave' +} + +export const codebuddyHookService = new ClaudeHookService({ + agent: 'codebuddy', + source: 'codebuddy', + displayName: 'CodeBuddy', + settings: { + configDirName: '.codebuddy', + scriptBaseName: 'codebuddy-hook', + usesWindowsCompatLauncher: true, + windowsHookShell: 'powershell' + }, + hookPlan: CODEBUDDY_MANAGED_HOOK_PLAN +}) diff --git a/src/main/codex/__fixtures__/codex-app-server-turn-endings.jsonl b/src/main/codex/__fixtures__/codex-app-server-turn-endings.jsonl new file mode 100644 index 00000000000..7889b24d113 --- /dev/null +++ b/src/main/codex/__fixtures__/codex-app-server-turn-endings.jsonl @@ -0,0 +1,172 @@ +{"case":"0.141.0-bad-model","t":371,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000001","status":{"type":"active"}}} +{"case":"0.141.0-bad-model","t":371,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000001","turn":{"id":"00000000-0000-7000-8000-000000000002","status":"inProgress"}}} +{"case":"0.141.0-bad-model","t":1745,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000001","turnId":"00000000-0000-7000-8000-000000000002","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000003","clientId":null,"content":[]}}} +{"case":"0.141.0-bad-model","t":1745,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000001","turnId":"00000000-0000-7000-8000-000000000002","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000003","clientId":null,"content":[]}}} +{"case":"0.141.0-bad-model","t":2831,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000001","status":{"type":"systemError"}}} +{"case":"0.141.0-bad-model","t":2831,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000001","turnId":"00000000-0000-7000-8000-000000000002","willRetry":false,"error":{"message":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-qa-nonexistent' model is not supported when using Codex with a ChatGPT account.\"}}","codexErrorInfo":"other"}}} +{"case":"0.141.0-bad-model","t":2831,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000001","turn":{"id":"00000000-0000-7000-8000-000000000002","status":"failed","error":{"message":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-qa-nonexistent' model is not supported when using Codex with a ChatGPT account.\"}}","codexErrorInfo":"other"},"durationMs":2488}}} +{"case":"0.141.0-bad-turn-cwd","t":1458,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000004","status":{"type":"active"}}} +{"case":"0.141.0-bad-turn-cwd","t":1458,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000004","turn":{"id":"00000000-0000-7000-8000-000000000005","status":"inProgress"}}} +{"case":"0.141.0-bad-turn-cwd","t":5792,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000004","turnId":"00000000-0000-7000-8000-000000000005","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000006","clientId":null,"content":[]}}} +{"case":"0.141.0-bad-turn-cwd","t":5792,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000004","turnId":"00000000-0000-7000-8000-000000000005","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000006","clientId":null,"content":[]}}} +{"case":"0.141.0-bad-turn-cwd","t":7645,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000004","turnId":"00000000-0000-7000-8000-000000000005","item":{"type":"agentMessage","id":"msg_1","text":""}}} +{"case":"0.141.0-bad-turn-cwd","t":7938,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000004","turnId":"00000000-0000-7000-8000-000000000005","item":{"type":"agentMessage","id":"msg_1","text":"pong"}}} +{"case":"0.141.0-bad-turn-cwd","t":7959,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000004","status":{"type":"idle"}}} +{"case":"0.141.0-bad-turn-cwd","t":7959,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000004","turn":{"id":"00000000-0000-7000-8000-000000000005","status":"completed","durationMs":6532}}} +{"case":"0.141.0-compact-overloaded","t":582,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000007","status":{"type":"active"}}} +{"case":"0.141.0-compact-overloaded","t":582,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000007","turn":{"id":"00000000-0000-7000-8000-000000000008","status":"inProgress"}}} +{"case":"0.141.0-compact-overloaded","t":2940,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000007","turnId":"00000000-0000-7000-8000-000000000008","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000009","clientId":null,"content":[]}}} +{"case":"0.141.0-compact-overloaded","t":2940,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000007","turnId":"00000000-0000-7000-8000-000000000008","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000009","clientId":null,"content":[]}}} +{"case":"0.141.0-compact-overloaded","t":3011,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000007","turnId":"00000000-0000-7000-8000-000000000008","item":{"type":"agentMessage","id":"msg_ok","text":"pong"}}} +{"case":"0.141.0-compact-overloaded","t":3011,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000007","turnId":"00000000-0000-7000-8000-000000000008","item":{"type":"agentMessage","id":"msg_ok","text":"pong"}}} +{"case":"0.141.0-compact-overloaded","t":3017,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000007","status":{"type":"idle"}}} +{"case":"0.141.0-compact-overloaded","t":3017,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000007","turn":{"id":"00000000-0000-7000-8000-000000000008","status":"completed","durationMs":2474}}} +{"case":"0.141.0-compact-overloaded","t":3076,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000007","status":{"type":"active"}}} +{"case":"0.141.0-compact-overloaded","t":3076,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000007","turn":{"id":"00000000-0000-7000-8000-000000000010","status":"inProgress"}}} +{"case":"0.141.0-compact-overloaded","t":3081,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000007","turnId":"00000000-0000-7000-8000-000000000010","item":{"type":"contextCompaction","id":"00000000-0000-7000-8000-000000000011"}}} +{"case":"0.141.0-compact-overloaded","t":3684,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000007","turnId":"00000000-0000-7000-8000-000000000010","willRetry":true,"error":{"message":"Reconnecting... 1/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Selected model is at capacity. Please try a different model."}}} +{"case":"0.141.0-compact-overloaded","t":4508,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000007","turnId":"00000000-0000-7000-8000-000000000010","willRetry":true,"error":{"message":"Reconnecting... 2/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Selected model is at capacity. Please try a different model."}}} +{"case":"0.141.0-compact-overloaded","t":5511,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000007","status":{"type":"systemError"}}} +{"case":"0.141.0-compact-overloaded","t":5511,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000007","turnId":"00000000-0000-7000-8000-000000000010","willRetry":false,"error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"}}} +{"case":"0.141.0-compact-overloaded","t":5511,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000007","turn":{"id":"00000000-0000-7000-8000-000000000010","status":"failed","error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"},"durationMs":2435}}} +{"case":"0.141.0-conn-refused","t":588,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000012","status":{"type":"active"}}} +{"case":"0.141.0-conn-refused","t":588,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000012","turn":{"id":"00000000-0000-7000-8000-000000000013","status":"inProgress"}}} +{"case":"0.141.0-conn-refused","t":700,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000012","turnId":"00000000-0000-7000-8000-000000000013","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000014","clientId":null,"content":[]}}} +{"case":"0.141.0-conn-refused","t":700,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000012","turnId":"00000000-0000-7000-8000-000000000013","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000014","clientId":null,"content":[]}}} +{"case":"0.141.0-conn-refused","t":1354,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000012","turnId":"00000000-0000-7000-8000-000000000013","willRetry":true,"error":{"message":"Reconnecting... 1/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"stream disconnected before completion: error sending request for url (http://127.0.0.1:9/v1/responses)"}}} +{"case":"0.141.0-conn-refused","t":2134,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000012","turnId":"00000000-0000-7000-8000-000000000013","willRetry":true,"error":{"message":"Reconnecting... 2/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"stream disconnected before completion: error sending request for url (http://127.0.0.1:9/v1/responses)"}}} +{"case":"0.141.0-conn-refused","t":3116,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000012","status":{"type":"systemError"}}} +{"case":"0.141.0-conn-refused","t":3116,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000012","turnId":"00000000-0000-7000-8000-000000000013","willRetry":false,"error":{"message":"stream disconnected before completion: error sending request for url (http://127.0.0.1:9/v1/responses)","codexErrorInfo":"other"}}} +{"case":"0.141.0-conn-refused","t":3116,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000012","turn":{"id":"00000000-0000-7000-8000-000000000013","status":"failed","error":{"message":"stream disconnected before completion: error sending request for url (http://127.0.0.1:9/v1/responses)","codexErrorInfo":"other"},"durationMs":2556}}} +{"case":"0.141.0-control","t":487,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000015","status":{"type":"active"}}} +{"case":"0.141.0-control","t":487,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000015","turn":{"id":"00000000-0000-7000-8000-000000000016","status":"inProgress"}}} +{"case":"0.141.0-control","t":2657,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000015","turnId":"00000000-0000-7000-8000-000000000016","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000017","clientId":null,"content":[]}}} +{"case":"0.141.0-control","t":2657,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000015","turnId":"00000000-0000-7000-8000-000000000016","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000017","clientId":null,"content":[]}}} +{"case":"0.141.0-control","t":4651,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000015","turnId":"00000000-0000-7000-8000-000000000016","item":{"type":"agentMessage","id":"msg_2","text":""}}} +{"case":"0.141.0-control","t":4661,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000015","turnId":"00000000-0000-7000-8000-000000000016","item":{"type":"agentMessage","id":"msg_2","text":"pong"}}} +{"case":"0.141.0-control","t":4811,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000015","status":{"type":"idle"}}} +{"case":"0.141.0-control","t":4811,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000015","turn":{"id":"00000000-0000-7000-8000-000000000016","status":"completed","durationMs":4351}}} +{"case":"0.141.0-interrupt","t":670,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000018","status":{"type":"active"}}} +{"case":"0.141.0-interrupt","t":670,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000018","turn":{"id":"00000000-0000-7000-8000-000000000019","status":"inProgress"}}} +{"case":"0.141.0-interrupt","t":2618,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000018","turnId":"00000000-0000-7000-8000-000000000019","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000020","clientId":null,"content":[]}}} +{"case":"0.141.0-interrupt","t":2618,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000018","turnId":"00000000-0000-7000-8000-000000000019","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000020","clientId":null,"content":[]}}} +{"case":"0.141.0-interrupt","t":4153,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000018","turnId":"00000000-0000-7000-8000-000000000019","item":{"type":"agentMessage","id":"msg_3","text":""}}} +{"case":"0.141.0-interrupt","t":5669,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000018","status":{"type":"idle"}}} +{"case":"0.141.0-interrupt","t":5669,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000018","turn":{"id":"00000000-0000-7000-8000-000000000019","status":"interrupted","durationMs":5027}}} +{"case":"0.141.0-model-too-new-a","t":1276,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000021","status":{"type":"active"}}} +{"case":"0.141.0-model-too-new-a","t":1276,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000021","turn":{"id":"00000000-0000-7000-8000-000000000022","status":"inProgress"}}} +{"case":"0.141.0-model-too-new-a","t":6105,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000021","turnId":"00000000-0000-7000-8000-000000000022","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000023","clientId":null,"content":[]}}} +{"case":"0.141.0-model-too-new-a","t":6105,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000021","turnId":"00000000-0000-7000-8000-000000000022","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000023","clientId":null,"content":[]}}} +{"case":"0.141.0-model-too-new-a","t":7424,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000021","status":{"type":"systemError"}}} +{"case":"0.141.0-model-too-new-a","t":7424,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000021","turnId":"00000000-0000-7000-8000-000000000022","willRetry":false,"error":{"message":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-6-astra' model requires a newer version of Codex. Please upgrade to the latest app or CLI and try again.\"}}","codexErrorInfo":"other"}}} +{"case":"0.141.0-model-too-new-a","t":7424,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000021","turn":{"id":"00000000-0000-7000-8000-000000000022","status":"failed","error":{"message":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-6-astra' model requires a newer version of Codex. Please upgrade to the latest app or CLI and try again.\"}}","codexErrorInfo":"other"},"durationMs":6195}}} +{"case":"0.141.0-model-too-new-b","t":387,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000024","status":{"type":"active"}}} +{"case":"0.141.0-model-too-new-b","t":387,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000024","turn":{"id":"00000000-0000-7000-8000-000000000025","status":"inProgress"}}} +{"case":"0.141.0-model-too-new-b","t":1162,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000024","turnId":"00000000-0000-7000-8000-000000000025","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000026","clientId":null,"content":[]}}} +{"case":"0.141.0-model-too-new-b","t":1162,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000024","turnId":"00000000-0000-7000-8000-000000000025","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000026","clientId":null,"content":[]}}} +{"case":"0.141.0-model-too-new-b","t":1993,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000024","status":{"type":"systemError"}}} +{"case":"0.141.0-model-too-new-b","t":1993,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000024","turnId":"00000000-0000-7000-8000-000000000025","willRetry":false,"error":{"message":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-6-astra' model requires a newer version of Codex. Please upgrade to the latest app or CLI and try again.\"}}","codexErrorInfo":"other"}}} +{"case":"0.141.0-model-too-new-b","t":1993,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000024","turn":{"id":"00000000-0000-7000-8000-000000000025","status":"failed","error":{"message":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-6-astra' model requires a newer version of Codex. Please upgrade to the latest app or CLI and try again.\"}}","codexErrorInfo":"other"},"durationMs":1633}}} +{"case":"0.141.0-overloaded-503","t":407,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000027","status":{"type":"active"}}} +{"case":"0.141.0-overloaded-503","t":407,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000027","turn":{"id":"00000000-0000-7000-8000-000000000028","status":"inProgress"}}} +{"case":"0.141.0-overloaded-503","t":574,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000027","turnId":"00000000-0000-7000-8000-000000000028","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000029","clientId":null,"content":[]}}} +{"case":"0.141.0-overloaded-503","t":574,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000027","turnId":"00000000-0000-7000-8000-000000000028","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000029","clientId":null,"content":[]}}} +{"case":"0.141.0-overloaded-503","t":1296,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000027","status":{"type":"systemError"}}} +{"case":"0.141.0-overloaded-503","t":1296,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000027","turnId":"00000000-0000-7000-8000-000000000028","willRetry":false,"error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"}}} +{"case":"0.141.0-overloaded-503","t":1296,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000027","turn":{"id":"00000000-0000-7000-8000-000000000028","status":"failed","error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"},"durationMs":921}}} +{"case":"0.141.0-overloaded-sse","t":510,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000030","status":{"type":"active"}}} +{"case":"0.141.0-overloaded-sse","t":510,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000030","turn":{"id":"00000000-0000-7000-8000-000000000031","status":"inProgress"}}} +{"case":"0.141.0-overloaded-sse","t":611,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000030","turnId":"00000000-0000-7000-8000-000000000031","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000032","clientId":null,"content":[]}}} +{"case":"0.141.0-overloaded-sse","t":611,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000030","turnId":"00000000-0000-7000-8000-000000000031","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000032","clientId":null,"content":[]}}} +{"case":"0.141.0-overloaded-sse","t":695,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000030","status":{"type":"systemError"}}} +{"case":"0.141.0-overloaded-sse","t":695,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000030","turnId":"00000000-0000-7000-8000-000000000031","willRetry":false,"error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"}}} +{"case":"0.141.0-overloaded-sse","t":695,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000030","turn":{"id":"00000000-0000-7000-8000-000000000031","status":"failed","error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"},"durationMs":213}}} +{"case":"0.141.0-stream-drop","t":510,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000033","status":{"type":"active"}}} +{"case":"0.141.0-stream-drop","t":510,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000033","turn":{"id":"00000000-0000-7000-8000-000000000034","status":"inProgress"}}} +{"case":"0.141.0-stream-drop","t":621,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000033","turnId":"00000000-0000-7000-8000-000000000034","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000035","clientId":null,"content":[]}}} +{"case":"0.141.0-stream-drop","t":621,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000033","turnId":"00000000-0000-7000-8000-000000000034","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000035","clientId":null,"content":[]}}} +{"case":"0.141.0-stream-drop","t":740,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000033","turnId":"00000000-0000-7000-8000-000000000034","willRetry":true,"error":{"message":"Reconnecting... 1/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"stream disconnected before completion: stream closed before response.completed"}}} +{"case":"0.141.0-stream-drop","t":942,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000033","turnId":"00000000-0000-7000-8000-000000000034","willRetry":true,"error":{"message":"Reconnecting... 2/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"stream disconnected before completion: stream closed before response.completed"}}} +{"case":"0.141.0-stream-drop","t":1320,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000033","status":{"type":"systemError"}}} +{"case":"0.141.0-stream-drop","t":1320,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000033","turnId":"00000000-0000-7000-8000-000000000034","willRetry":false,"error":{"message":"stream disconnected before completion: stream closed before response.completed","codexErrorInfo":"other"}}} +{"case":"0.141.0-stream-drop","t":1320,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000033","turn":{"id":"00000000-0000-7000-8000-000000000034","status":"failed","error":{"message":"stream disconnected before completion: stream closed before response.completed","codexErrorInfo":"other"},"durationMs":838}}} +{"case":"0.158.0-bad-model","t":1232,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000036","status":{"type":"active"}}} +{"case":"0.158.0-bad-model","t":1232,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000036","turn":{"id":"00000000-0000-7000-8000-000000000037","status":"inProgress"}}} +{"case":"0.158.0-bad-model","t":7177,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000036","turnId":"00000000-0000-7000-8000-000000000037","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000038","clientId":null,"content":[]}}} +{"case":"0.158.0-bad-model","t":7215,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000036","turnId":"00000000-0000-7000-8000-000000000037","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000038","clientId":null,"content":[]}}} +{"case":"0.158.0-bad-model","t":8236,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000036","status":{"type":"systemError"}}} +{"case":"0.158.0-bad-model","t":8236,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000036","turnId":"00000000-0000-7000-8000-000000000037","willRetry":false,"error":{"message":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-qa-nonexistent' model is not supported when using Codex with a ChatGPT account.\"}}","codexErrorInfo":"other"}}} +{"case":"0.158.0-bad-model","t":8257,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000036","turn":{"id":"00000000-0000-7000-8000-000000000037","status":"failed","error":{"message":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-qa-nonexistent' model is not supported when using Codex with a ChatGPT account.\"}}","codexErrorInfo":"other"},"durationMs":7576}}} +{"case":"0.158.0-bad-turn-cwd","t":4437,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000039","status":{"type":"active"}}} +{"case":"0.158.0-bad-turn-cwd","t":4438,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000039","turn":{"id":"00000000-0000-7000-8000-000000000040","status":"inProgress"}}} +{"case":"0.158.0-bad-turn-cwd","t":7840,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000039","turnId":"00000000-0000-7000-8000-000000000040","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000041","clientId":null,"content":[]}}} +{"case":"0.158.0-bad-turn-cwd","t":7851,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000039","turnId":"00000000-0000-7000-8000-000000000040","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000041","clientId":null,"content":[]}}} +{"case":"0.158.0-bad-turn-cwd","t":9220,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000039","turnId":"00000000-0000-7000-8000-000000000040","item":{"type":"agentMessage","id":"msg_4","text":""}}} +{"case":"0.158.0-bad-turn-cwd","t":9421,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000039","turnId":"00000000-0000-7000-8000-000000000040","item":{"type":"agentMessage","id":"msg_4","text":"pong"}}} +{"case":"0.158.0-bad-turn-cwd","t":9566,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000039","status":{"type":"idle"}}} +{"case":"0.158.0-bad-turn-cwd","t":9566,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000039","turn":{"id":"00000000-0000-7000-8000-000000000040","status":"completed","durationMs":5172}}} +{"case":"0.158.0-compact-overloaded","t":943,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000042","status":{"type":"active"}}} +{"case":"0.158.0-compact-overloaded","t":944,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000042","turn":{"id":"00000000-0000-7000-8000-000000000043","status":"inProgress"}}} +{"case":"0.158.0-compact-overloaded","t":5760,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000042","turnId":"00000000-0000-7000-8000-000000000043","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000044","clientId":null,"content":[]}}} +{"case":"0.158.0-compact-overloaded","t":5765,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000042","turnId":"00000000-0000-7000-8000-000000000043","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000044","clientId":null,"content":[]}}} +{"case":"0.158.0-compact-overloaded","t":5770,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000042","turnId":"00000000-0000-7000-8000-000000000043","item":{"type":"agentMessage","id":"msg_ok","text":"pong"}}} +{"case":"0.158.0-compact-overloaded","t":5771,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000042","turnId":"00000000-0000-7000-8000-000000000043","item":{"type":"agentMessage","id":"msg_ok","text":"pong"}}} +{"case":"0.158.0-compact-overloaded","t":5795,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000042","status":{"type":"idle"}}} +{"case":"0.158.0-compact-overloaded","t":5795,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000042","turn":{"id":"00000000-0000-7000-8000-000000000043","status":"completed","durationMs":4899}}} +{"case":"0.158.0-compact-overloaded","t":5820,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000042","status":{"type":"active"}}} +{"case":"0.158.0-compact-overloaded","t":5820,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000042","turn":{"id":"00000000-0000-7000-8000-000000000045","status":"inProgress"}}} +{"case":"0.158.0-compact-overloaded","t":5821,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000042","turnId":"00000000-0000-7000-8000-000000000045","item":{"type":"contextCompaction","id":"00000000-0000-7000-8000-000000000046"}}} +{"case":"0.158.0-compact-overloaded","t":6421,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000042","turnId":"00000000-0000-7000-8000-000000000045","willRetry":true,"error":{"message":"Reconnecting... 1/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Selected model is at capacity. Please try a different model."}}} +{"case":"0.158.0-compact-overloaded","t":7232,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000042","turnId":"00000000-0000-7000-8000-000000000045","willRetry":true,"error":{"message":"Reconnecting... 2/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Selected model is at capacity. Please try a different model."}}} +{"case":"0.158.0-compact-overloaded","t":8204,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000042","status":{"type":"systemError"}}} +{"case":"0.158.0-compact-overloaded","t":8204,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000042","turnId":"00000000-0000-7000-8000-000000000045","willRetry":false,"error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"}}} +{"case":"0.158.0-compact-overloaded","t":8207,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000042","turn":{"id":"00000000-0000-7000-8000-000000000045","status":"failed","error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"},"durationMs":2388}}} +{"case":"0.158.0-conn-refused","t":1343,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000047","status":{"type":"active"}}} +{"case":"0.158.0-conn-refused","t":1343,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000047","turn":{"id":"00000000-0000-7000-8000-000000000048","status":"inProgress"}}} +{"case":"0.158.0-conn-refused","t":2002,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000049","clientId":null,"content":[]}}} +{"case":"0.158.0-conn-refused","t":2013,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000049","clientId":null,"content":[]}}} +{"case":"0.158.0-conn-refused","t":2616,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","willRetry":true,"error":{"message":"Reconnecting... waiting for network","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Connection failed: error sending request"}}} +{"case":"0.158.0-conn-refused","t":8185,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","willRetry":true,"error":{"message":"Reconnecting... waiting for network","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Connection failed: error sending request"}}} +{"case":"0.158.0-conn-refused","t":18778,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","willRetry":true,"error":{"message":"Reconnecting... waiting for network","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Connection failed: error sending request"}}} +{"case":"0.158.0-conn-refused","t":39406,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","willRetry":true,"error":{"message":"Reconnecting... waiting for network","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Connection failed: error sending request"}}} +{"case":"0.158.0-conn-refused","t":79975,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","willRetry":true,"error":{"message":"Reconnecting... waiting for network","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Connection failed: error sending request"}}} +{"case":"0.158.0-conn-refused","t":140587,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","willRetry":true,"error":{"message":"Reconnecting... waiting for network","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Connection failed: error sending request"}}} +{"case":"0.158.0-conn-refused","t":201225,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000047","turnId":"00000000-0000-7000-8000-000000000048","willRetry":true,"error":{"message":"Reconnecting... waiting for network","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"Connection failed: error sending request"}}} +{"case":"0.158.0-control","t":3620,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000050","status":{"type":"active"}}} +{"case":"0.158.0-control","t":3621,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000050","turn":{"id":"00000000-0000-7000-8000-000000000051","status":"inProgress"}}} +{"case":"0.158.0-control","t":5019,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000050","turnId":"00000000-0000-7000-8000-000000000051","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000052","clientId":null,"content":[]}}} +{"case":"0.158.0-control","t":5254,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000050","turnId":"00000000-0000-7000-8000-000000000051","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000052","clientId":null,"content":[]}}} +{"case":"0.158.0-control","t":7276,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000050","turnId":"00000000-0000-7000-8000-000000000051","item":{"type":"agentMessage","id":"msg_5","text":""}}} +{"case":"0.158.0-control","t":7480,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000050","turnId":"00000000-0000-7000-8000-000000000051","item":{"type":"agentMessage","id":"msg_5","text":"pong"}}} +{"case":"0.158.0-control","t":7659,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000050","status":{"type":"idle"}}} +{"case":"0.158.0-control","t":7659,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000050","turn":{"id":"00000000-0000-7000-8000-000000000051","status":"completed","durationMs":4520}}} +{"case":"0.158.0-interrupt","t":929,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000053","status":{"type":"active"}}} +{"case":"0.158.0-interrupt","t":929,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000053","turn":{"id":"00000000-0000-7000-8000-000000000054","status":"inProgress"}}} +{"case":"0.158.0-interrupt","t":2320,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000053","turnId":"00000000-0000-7000-8000-000000000054","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000055","clientId":null,"content":[]}}} +{"case":"0.158.0-interrupt","t":2325,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000053","turnId":"00000000-0000-7000-8000-000000000054","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000055","clientId":null,"content":[]}}} +{"case":"0.158.0-interrupt","t":4063,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000053","turnId":"00000000-0000-7000-8000-000000000054","item":{"type":"agentMessage","id":"msg_6","text":""}}} +{"case":"0.158.0-interrupt","t":5582,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000053","status":{"type":"idle"}}} +{"case":"0.158.0-interrupt","t":5582,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000053","turn":{"id":"00000000-0000-7000-8000-000000000054","status":"interrupted","durationMs":4711}}} +{"case":"0.158.0-overloaded-503","t":455,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000056","status":{"type":"active"}}} +{"case":"0.158.0-overloaded-503","t":455,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000056","turn":{"id":"00000000-0000-7000-8000-000000000057","status":"inProgress"}}} +{"case":"0.158.0-overloaded-503","t":1114,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000056","turnId":"00000000-0000-7000-8000-000000000057","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000058","clientId":null,"content":[]}}} +{"case":"0.158.0-overloaded-503","t":1131,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000056","turnId":"00000000-0000-7000-8000-000000000057","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000058","clientId":null,"content":[]}}} +{"case":"0.158.0-overloaded-503","t":1722,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000056","status":{"type":"systemError"}}} +{"case":"0.158.0-overloaded-503","t":1722,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000056","turnId":"00000000-0000-7000-8000-000000000057","willRetry":false,"error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"}}} +{"case":"0.158.0-overloaded-503","t":1753,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000056","turn":{"id":"00000000-0000-7000-8000-000000000057","status":"failed","error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"},"durationMs":1302}}} +{"case":"0.158.0-overloaded-sse","t":944,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000059","status":{"type":"active"}}} +{"case":"0.158.0-overloaded-sse","t":944,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000059","turn":{"id":"00000000-0000-7000-8000-000000000060","status":"inProgress"}}} +{"case":"0.158.0-overloaded-sse","t":1196,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000059","turnId":"00000000-0000-7000-8000-000000000060","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000061","clientId":null,"content":[]}}} +{"case":"0.158.0-overloaded-sse","t":1199,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000059","turnId":"00000000-0000-7000-8000-000000000060","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000061","clientId":null,"content":[]}}} +{"case":"0.158.0-overloaded-sse","t":1204,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000059","status":{"type":"systemError"}}} +{"case":"0.158.0-overloaded-sse","t":1204,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000059","turnId":"00000000-0000-7000-8000-000000000060","willRetry":false,"error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"}}} +{"case":"0.158.0-overloaded-sse","t":1206,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000059","turn":{"id":"00000000-0000-7000-8000-000000000060","status":"failed","error":{"message":"Selected model is at capacity. Please try a different model.","codexErrorInfo":"serverOverloaded"},"durationMs":481}}} +{"case":"0.158.0-stream-drop","t":1136,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000062","status":{"type":"active"}}} +{"case":"0.158.0-stream-drop","t":1136,"method":"turn/started","params":{"threadId":"00000000-0000-7000-8000-000000000062","turn":{"id":"00000000-0000-7000-8000-000000000063","status":"inProgress"}}} +{"case":"0.158.0-stream-drop","t":1721,"method":"item/started","params":{"threadId":"00000000-0000-7000-8000-000000000062","turnId":"00000000-0000-7000-8000-000000000063","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000064","clientId":null,"content":[]}}} +{"case":"0.158.0-stream-drop","t":1733,"method":"item/completed","params":{"threadId":"00000000-0000-7000-8000-000000000062","turnId":"00000000-0000-7000-8000-000000000063","item":{"type":"userMessage","id":"00000000-0000-7000-8000-000000000064","clientId":null,"content":[]}}} +{"case":"0.158.0-stream-drop","t":1738,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000062","turnId":"00000000-0000-7000-8000-000000000063","willRetry":true,"error":{"message":"Reconnecting... 1/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"stream disconnected before completion: stream closed before response.completed"}}} +{"case":"0.158.0-stream-drop","t":1959,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000062","turnId":"00000000-0000-7000-8000-000000000063","willRetry":true,"error":{"message":"Reconnecting... 2/2","codexErrorInfo":{"responseStreamDisconnected":{"httpStatusCode":null}},"additionalDetails":"stream disconnected before completion: stream closed before response.completed"}}} +{"case":"0.158.0-stream-drop","t":2514,"method":"thread/status/changed","params":{"threadId":"00000000-0000-7000-8000-000000000062","status":{"type":"systemError"}}} +{"case":"0.158.0-stream-drop","t":2514,"method":"error","params":{"threadId":"00000000-0000-7000-8000-000000000062","turnId":"00000000-0000-7000-8000-000000000063","willRetry":false,"error":{"message":"stream disconnected before completion: stream closed before response.completed","codexErrorInfo":"other"}}} +{"case":"0.158.0-stream-drop","t":2546,"method":"turn/completed","params":{"threadId":"00000000-0000-7000-8000-000000000062","turn":{"id":"00000000-0000-7000-8000-000000000063","status":"failed","error":{"message":"stream disconnected before completion: stream closed before response.completed","codexErrorInfo":"other"},"durationMs":1761}}} diff --git a/src/main/codex/codex-app-server-connection.test.ts b/src/main/codex/codex-app-server-connection.test.ts index 4e7cd016bd6..a4ebd72cf01 100644 --- a/src/main/codex/codex-app-server-connection.test.ts +++ b/src/main/codex/codex-app-server-connection.test.ts @@ -11,8 +11,12 @@ import { type CodexAppServerConnection, type CodexAppServerConnectionHandlers } from './codex-app-server-connection' +import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from './codex-app-server-posix-supervisor' import { isCodexAppServerUnsupportedError } from './codex-app-server-session' +// close() waits out the supervisor's own stop before forcing the tree. +const GRACEFUL_EXIT_MS = process.platform === 'win32' ? 1_500 : PROVIDER_SUPERVISOR_MAX_STOP_MS + const originalCodexHome = process.env.CODEX_HOME afterEach(() => { @@ -392,7 +396,7 @@ describe('openCodexAppServerConnection', () => { openCodexAppServerConnection({ command: 'codex', args: ['app-server'] }, {}, spawnImpl) ) - await vi.advanceTimersByTimeAsync(5_000) + await vi.advanceTimersByTimeAsync(GRACEFUL_EXIT_MS + 3_500) const error = (await opening) as Error & { connection?: CodexAppServerConnection } expect(error.name).toBe('CodexAppServerHandshakeExitUnprovenError') @@ -434,7 +438,7 @@ describe('openCodexAppServerConnection', () => { }) const closing = connection.close() - await vi.advanceTimersByTimeAsync(2_000) + await vi.advanceTimersByTimeAsync(GRACEFUL_EXIT_MS + 500) await closing await vi.waitFor(() => expect(child.kill).toHaveBeenCalledWith('SIGKILL')) @@ -468,7 +472,7 @@ describe('openCodexAppServerConnection', () => { const first = connection.close() const second = connection.close() - await vi.advanceTimersByTimeAsync(4_100) + await vi.advanceTimersByTimeAsync(GRACEFUL_EXIT_MS + 2_600) await expect(Promise.all([first, second])).resolves.toEqual([true, true]) expect(child.kill.mock.calls.map(([signal]) => signal)).toEqual(['SIGSTOP', 'SIGKILL']) @@ -485,7 +489,7 @@ describe('openCodexAppServerConnection', () => { ) const first = connection.close() - await vi.advanceTimersByTimeAsync(5_000) + await vi.advanceTimersByTimeAsync(GRACEFUL_EXIT_MS + 3_500) await expect(first).resolves.toBe(false) child.emit('exit', 0, null) @@ -579,6 +583,36 @@ describe('openCodexAppServerConnection', () => { await connection.close() }) + it('delivers a notification beyond the daemon wire limit whole, never as an oversized frame', async () => { + const { child, spawnImpl } = stubChild() + answerInitialize(child) + const frames: string[] = [] + const deltas: unknown[] = [] + const connection = await openCodexAppServerConnection( + { command: 'codex', args: ['app-server'] }, + { + onUnhandledFrame: (kind) => frames.push(kind), + onNotification: (method, params) => { + if (method === 'item/commandExecution/outputDelta') { + deltas.push(params) + } + } + }, + spawnImpl + ) + + const params = { threadId: 'thread-1', itemId: 'exec-1', delta: '' } + params.delta = 'x'.repeat(16 * 1024 * 1024 + 1) + child.stdout.write( + `${JSON.stringify({ method: 'item/commandExecution/outputDelta', params })}\n` + ) + + await vi.waitFor(() => expect(deltas).toEqual([params])) + expect(frames).toEqual([]) + expect(connection.closed).toBe(false) + await connection.close() + }) + it('keeps malformed and non-object JSON non-fatal and processes the next record', async () => { const { child, spawnImpl } = stubChild() answerInitialize(child) diff --git a/src/main/codex/codex-app-server-connection.ts b/src/main/codex/codex-app-server-connection.ts index c0ac7f07400..5bed3b594d7 100644 --- a/src/main/codex/codex-app-server-connection.ts +++ b/src/main/codex/codex-app-server-connection.ts @@ -1,11 +1,13 @@ import { spawnProcess } from '../../shared/child-process/run-process' import { RetryableProcessExitProof } from '../../shared/child-process/retryable-process-exit-proof' -import { createProviderSpawnSpec } from './codex-app-server-posix-supervisor' +import { + createProviderSpawnSpec, + PROVIDER_SUPERVISOR_MAX_STOP_MS +} from './codex-app-server-posix-supervisor' import { buildCodexAppServerExitError } from './codex-app-server-exit-error' import { initializeCodexAppServerConnection } from './codex-app-server-handshake' import { CodexAppServerHandshakeExitUnprovenError } from './codex-app-server-handshake-exit-proof' import { terminateCodexAppServerProcessTree } from './codex-app-server-process-teardown' -import { CODEX_SPAWN_TOKEN_ENV } from './codex-structured-owner-identity' import { waitForProcessExitUntil } from './codex-process-exit-deadline' import { CodexAppServerTimeoutError, @@ -44,7 +46,7 @@ export type CodexAppServerLaunch = { } const DEFAULT_REQUEST_TIMEOUT_MS = 30_000 -const GRACEFUL_EXIT_MS = 1_500 +export const GRACEFUL_EXIT_MS = 1_500 const FORCED_EXIT_MS = 1_000 const STDERR_TAIL_MAX_BYTES = 8192 @@ -64,12 +66,11 @@ export async function openCodexAppServerConnection( } const spawnSpec = createProviderSpawnSpec(launch, childEnv, process.platform) const child = spawnImpl(spawnSpec) - const spawnToken = launch.env?.[CODEX_SPAWN_TOKEN_ENV] function terminateProcessTree(): Promise { // The supervisor and provider own separate POSIX groups so the supervisor can prove the // provider group empty before relaying its exit. Forced wrapper teardown uses descendant proof. - return terminateCodexAppServerProcessTree(child, spawnToken) + return terminateCodexAppServerProcessTree(child) } let stderrTail = '' @@ -249,7 +250,11 @@ export async function openCodexAppServerConnection( // Already destroyed; the reap below still runs. } if (!exited) { - await waitForProcessExitUntil(exitPromise, GRACEFUL_EXIT_MS) + // The POSIX supervisor stops its own provider group; forcing it any sooner can orphan it. + await waitForProcessExitUntil( + exitPromise, + process.platform === 'win32' ? GRACEFUL_EXIT_MS : PROVIDER_SUPERVISOR_MAX_STOP_MS + ) if (!exited) { const treeExited = await terminateProcessTree() if (!treeExited) { diff --git a/src/main/codex/codex-app-server-posix-supervisor.integration.test.ts b/src/main/codex/codex-app-server-posix-supervisor.integration.test.ts new file mode 100644 index 00000000000..846afaa973f --- /dev/null +++ b/src/main/codex/codex-app-server-posix-supervisor.integration.test.ts @@ -0,0 +1,355 @@ +import { spawn, type ChildProcess } from 'node:child_process' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + POSIX_PROVIDER_SUPERVISOR_SCRIPT, + PROVIDER_SIGTERM_GRACE_MS, + PROVIDER_STDIN_END_GRACE_MS, + PROVIDER_SUPERVISOR_MAX_STOP_MS, + supervisedPosixLaunch, + type ProviderSupervisorOptions +} from './codex-app-server-posix-supervisor' + +// The provider leads its own group; its grandchild shares that group and ignores SIGTERM. +const PROVIDER = String.raw` + const { spawn } = require('node:child_process') + if (process.env.ORCA_TEST_PROVIDER_IGNORES_SIGTERM) process.on('SIGTERM', () => {}) + if (process.env.ORCA_TEST_PROVIDER_SIGNAL_FILE) { + process.on('SIGTERM', () => { + require('node:fs').writeFileSync(process.env.ORCA_TEST_PROVIDER_SIGNAL_FILE, 'SIGTERM') + process.exit(0) + }) + } + process.stdout.on('error', () => {}) + const grandchild = spawn( + process.execPath, + ['-e', "process.on('SIGTERM', () => {}); process.stdout.write('armed'); setInterval(() => {}, 60000)"], + { stdio: ['ignore', 'pipe', 'ignore'] } + ) + grandchild.stdout.once('data', () => { + process.stdout.write(JSON.stringify({ provider: process.pid, grandchild: grandchild.pid }) + '\n') + if (process.env.ORCA_TEST_PROVIDER_STREAMS_OUTPUT) setInterval(() => process.stdout.write('.'), 2) + }) + setInterval(() => {}, 60000) +` + +// Exits the moment its stdin ends, as Codex does on a normal close. +const EXITS_ON_STDIN_END_PROVIDER = String.raw` + process.stdin.on('end', () => process.exit(0)).resume() + process.stdout.write(JSON.stringify({ provider: process.pid }) + '\n') +` + +// Ignores stdin end and SIGTERM, recording when SIGTERM arrived, so only SIGKILL ends it. +const RECORDS_SIGTERM_PROVIDER = String.raw` + process.on('SIGTERM', () => { + require('node:fs').writeFileSync(process.env.ORCA_TEST_PROVIDER_SIGNAL_FILE, String(Date.now())) + }) + process.stdout.write(JSON.stringify({ provider: process.pid }) + '\n') + setInterval(() => {}, 60000) +` + +// Stands in for Orca: launches the supervisor as its own child, then can be killed outright. A +// second child holds the supervisor's stdin open, so only the parent-death watch can notice. +// A clean-quit owner has no holder and exits normally on SIGUSR2, the way Orca quits. +const OWNER = String.raw` + const { spawn } = require('node:child_process') + const quitsCleanly = Boolean(process.env.ORCA_TEST_OWNER_QUITS_CLEANLY) + if (quitsCleanly) process.on('SIGUSR2', () => process.exit(0)) + const spec = JSON.parse(Buffer.from(process.env.ORCA_PROVIDER_SUPERVISOR_SPEC, 'base64').toString()) + spec.ownerPid = process.pid + const supervisor = spawn(process.execPath, ['-e', process.env.ORCA_TEST_SUPERVISOR_SCRIPT], { + env: { ...process.env, ORCA_PROVIDER_SUPERVISOR_SPEC: Buffer.from(JSON.stringify(spec)).toString('base64') }, + stdio: ['pipe', 'pipe', 'ignore'], + detached: true + }) + const holder = quitsCleanly + ? null + : spawn(process.execPath, ['-e', 'setInterval(() => {}, 60000)'], { + stdio: ['ignore', supervisor.stdin, 'ignore'] + }) + process.stdout.write(JSON.stringify({ supervisor: supervisor.pid, ...(holder && { holder: holder.pid }) }) + '\n') + supervisor.stdout.pipe(process.stdout) + setInterval(() => {}, 60000) +` + +// Preloaded into the supervisor: signals it the instant its provider exists, the spawn window. +const SIGNAL_AFTER_SPAWN_PRELOAD = String.raw` + const childProcess = require('node:child_process') + const spawn = childProcess.spawn + childProcess.spawn = (...args) => { + const child = spawn(...args) + require('node:fs').writeFileSync(process.env.ORCA_TEST_PROVIDER_PID_FILE, String(child.pid)) + process.kill(process.pid, 'SIGTERM') + return child + } +` + +const recordedPids = new Set() +const tempDirs: string[] = [] + +function tempDir(): string { + const dir = mkdtempSync(join(tmpdir(), 'orca-supervisor-')) + tempDirs.push(dir) + return dir +} + +function alive(pid: number): boolean { + try { + process.kill(pid, 0) + return true + } catch (error) { + return !(error instanceof Error && 'code' in error && error.code === 'ESRCH') + } +} + +async function waitFor(predicate: () => boolean, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs + while (!predicate()) { + if (Date.now() >= deadline) { + return false + } + await new Promise((resolve) => setTimeout(resolve, 25)) + } + return true +} + +function readPids(child: ChildProcess, keys: readonly string[]): Promise> { + return new Promise((resolve, reject) => { + const pids: Record = {} + let buffered = '' + const timeout = setTimeout(() => reject(new Error(`no ${keys.join('/')} pids`)), 10_000) + const onData = (chunk: Buffer): void => { + buffered += chunk.toString() + const lines = buffered.split('\n') + buffered = lines.pop() ?? '' + for (const line of lines) { + const parsed: unknown = JSON.parse(line) + for (const [key, pid] of Object.entries(parsed ?? {})) { + if (typeof pid === 'number') { + pids[key] = pid + recordedPids.add(pid) + } + } + } + if (keys.every((key) => key in pids)) { + clearTimeout(timeout) + // Later output is not pids; the stream keeps flowing without a listener. + child.stdout!.off('data', onData) + resolve(pids) + } + } + child.stdout!.on('data', onData) + }) +} + +function launchSupervisor( + options: ProviderSupervisorOptions, + env: Record = {}, + provider: { command: string; args: string[] } = { + command: process.execPath, + args: ['-e', PROVIDER] + }, + nodeArgs: string[] = [] +): { supervisor: ChildProcess; exit: Promise<{ code: number | null; signal: string | null }> } { + const launch = supervisedPosixLaunch(provider, { ...process.env, ...env }, options) + const supervisor = spawn(launch.command, [...nodeArgs, ...launch.args], { + env: launch.env, + stdio: ['pipe', 'pipe', 'ignore'], + detached: true + }) + recordedPids.add(supervisor.pid!) + const exit = new Promise<{ code: number | null; signal: string | null }>((resolve) => + supervisor.once('exit', (code, signal) => resolve({ code, signal })) + ) + return { supervisor, exit } +} + +async function launchUnderOwner( + options: ProviderSupervisorOptions, + env: Record = {} +): Promise<{ owner: ChildProcess; pids: Record }> { + const launch = supervisedPosixLaunch( + { command: process.execPath, args: ['-e', PROVIDER] }, + { ...process.env, ...env }, + options + ) + const owner = spawn(process.execPath, ['-e', OWNER], { + env: { ...launch.env, ORCA_TEST_SUPERVISOR_SCRIPT: POSIX_PROVIDER_SUPERVISOR_SCRIPT }, + stdio: ['ignore', 'pipe', 'ignore'] + }) + recordedPids.add(owner.pid!) + const pids = await readPids(owner, ['supervisor', 'provider', 'grandchild']) + return { owner, pids } +} + +afterEach(() => { + for (const pid of recordedPids) { + if (alive(pid)) { + process.kill(pid, 'SIGKILL') + } + } + recordedPids.clear() + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }) + } +}) + +describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processes', () => { + it('reaps the provider group on SIGTERM and exits only after the group is gone', async () => { + const { supervisor, exit } = launchSupervisor({ sigtermGraceMs: 300 }) + const { provider, grandchild } = await readPids(supervisor, ['provider', 'grandchild']) + + let groupAliveAtExit: boolean | null = null + void exit.then(() => { + groupAliveAtExit = alive(-provider) + }) + supervisor.kill('SIGTERM') + + await expect(exit).resolves.toEqual({ code: null, signal: 'SIGTERM' }) + expect(groupAliveAtExit).toBe(false) + expect(alive(provider)).toBe(false) + expect(alive(grandchild)).toBe(false) + }) + + it('escalates a SIGTERM-ignoring provider to SIGKILL after the grace from the spec', async () => { + const graceMs = 200 + const { supervisor, exit } = launchSupervisor( + { sigtermGraceMs: graceMs }, + { ORCA_TEST_PROVIDER_IGNORES_SIGTERM: '1' } + ) + const { provider, grandchild } = await readPids(supervisor, ['provider', 'grandchild']) + + const signalledAt = Date.now() + supervisor.kill('SIGTERM') + const exited = await Promise.race([exit, new Promise((resolve) => setTimeout(resolve, 5_000))]) + + expect(exited).toEqual({ code: null, signal: 'SIGTERM' }) + expect(Date.now() - signalledAt).toBeGreaterThanOrEqual(graceMs) + expect(Date.now() - signalledAt).toBeLessThan(PROVIDER_SIGTERM_GRACE_MS) + expect(alive(provider)).toBe(false) + expect(alive(grandchild)).toBe(false) + }) + + it('reaps a provider spawned in the instant before a stop arrives', async () => { + const dir = tempDir() + const preload = join(dir, 'signal-after-spawn.js') + const pidFile = join(dir, 'provider-pid') + writeFileSync(preload, SIGNAL_AFTER_SPAWN_PRELOAD) + const { exit } = launchSupervisor( + { sigtermGraceMs: 300 }, + { ORCA_TEST_PROVIDER_PID_FILE: pidFile }, + { command: process.execPath, args: ['-e', 'setInterval(() => {}, 60000)'] }, + ['--require', preload] + ) + + await expect(exit).resolves.toEqual({ code: null, signal: 'SIGTERM' }) + const provider = Number(readFileSync(pidFile, 'utf8')) + recordedPids.add(provider) + expect(await waitFor(() => !alive(provider), 3_000)).toBe(true) + }) + + it('never spawns the provider when its owner is not its parent at start', async () => { + const marker = join(tempDir(), 'provider-started') + const { exit } = launchSupervisor( + { ownerPid: process.pid === 1 ? 2 : 1 }, + {}, + { command: 'touch', args: [marker] } + ) + + await expect(exit).resolves.toEqual({ code: 1, signal: null }) + await new Promise((resolve) => setTimeout(resolve, 200)) + expect(existsSync(marker)).toBe(false) + }) + + it.each([ + ['', {}], + // Output after the owner's death meets a closed pipe, which must not end the supervisor first. + [' while the provider is writing output', { ORCA_TEST_PROVIDER_STREAMS_OUTPUT: '1' }] + ])('reaps the provider group when its owner dies%s', async (_, env) => { + const graceMs = 300 + const { owner, pids } = await launchUnderOwner({ sigtermGraceMs: graceMs }, env) + + const killedAt = Date.now() + owner.kill('SIGKILL') + + expect(await waitFor(() => !alive(-pids.provider), 3_000)).toBe(true) + // The grandchild ignores SIGTERM, so the group lasts until the grace ends in SIGKILL. + expect(Date.now() - killedAt).toBeGreaterThanOrEqual(graceMs) + expect(await waitFor(() => !alive(pids.supervisor), 3_000)).toBe(true) + expect(alive(pids.grandchild)).toBe(false) + }) + + it('closes a provider that exits on stdin end without waiting out any grace', async () => { + const { supervisor, exit } = launchSupervisor( + {}, + {}, + { + command: process.execPath, + args: ['-e', EXITS_ON_STDIN_END_PROVIDER] + } + ) + const { provider } = await readPids(supervisor, ['provider']) + + const endedAt = Date.now() + supervisor.stdin!.end() + + await expect(exit).resolves.toEqual({ code: 0, signal: null }) + expect(Date.now() - endedAt).toBeLessThan(PROVIDER_STDIN_END_GRACE_MS) + expect(alive(provider)).toBe(false) + }) + + it('gives a provider 1 s after stdin end, then 3 s after SIGTERM before SIGKILL', async () => { + const signalFile = join(tempDir(), 'provider-sigterm-at') + const { supervisor, exit } = launchSupervisor( + {}, + { ORCA_TEST_PROVIDER_SIGNAL_FILE: signalFile }, + { command: process.execPath, args: ['-e', RECORDS_SIGTERM_PROVIDER] } + ) + const { provider } = await readPids(supervisor, ['provider']) + + const endedAt = Date.now() + supervisor.stdin!.end() + const exited = await exit + const exitedAt = Date.now() + const signalledAt = Number(readFileSync(signalFile, 'utf8')) + + expect(exited).toEqual({ code: 137, signal: null }) + // Timers may fire a tick early against another process's clock. + expect(signalledAt - endedAt).toBeGreaterThanOrEqual(1_000 - 20) + expect(exitedAt - signalledAt).toBeGreaterThanOrEqual(3_000 - 20) + expect(exitedAt - endedAt).toBeLessThan(PROVIDER_SUPERVISOR_MAX_STOP_MS + 1_000) + expect(alive(provider)).toBe(false) + }) + + it('reaps the provider group and exits when its owner quits cleanly', async () => { + const { owner, pids } = await launchUnderOwner( + { sigtermGraceMs: 300 }, + { ORCA_TEST_OWNER_QUITS_CLEANLY: '1' } + ) + const ownerExit = new Promise((resolve) => + owner.once('exit', (code, signal) => resolve({ code, signal })) + ) + + owner.kill('SIGUSR2') + + await expect(ownerExit).resolves.toEqual({ code: 0, signal: null }) + expect(await waitFor(() => !alive(-pids.provider), 3_000)).toBe(true) + expect(await waitFor(() => !alive(pids.supervisor), 3_000)).toBe(true) + expect(alive(pids.grandchild)).toBe(false) + }) + + it('asks the provider to stop with SIGTERM when its owner dies', async () => { + const signalFile = join(tempDir(), 'provider-signal') + const { owner, pids } = await launchUnderOwner( + { sigtermGraceMs: 300 }, + { ORCA_TEST_PROVIDER_SIGNAL_FILE: signalFile } + ) + + owner.kill('SIGKILL') + + expect(await waitFor(() => !alive(-pids.provider), 3_000)).toBe(true) + expect(existsSync(signalFile) && readFileSync(signalFile, 'utf8')).toBe('SIGTERM') + }) +}) diff --git a/src/main/codex/codex-app-server-posix-supervisor.test.ts b/src/main/codex/codex-app-server-posix-supervisor.test.ts index f51157a443c..6dbe03e5b2a 100644 --- a/src/main/codex/codex-app-server-posix-supervisor.test.ts +++ b/src/main/codex/codex-app-server-posix-supervisor.test.ts @@ -3,6 +3,8 @@ import type { CodexAppServerLaunch } from './codex-app-server-connection' import { createProviderSpawnSpec, POSIX_PROVIDER_SUPERVISOR_SCRIPT, + PROVIDER_SIGTERM_GRACE_MS, + PROVIDER_STDIN_END_GRACE_MS, supervisedPosixLaunch } from './codex-app-server-posix-supervisor' @@ -27,7 +29,10 @@ describe('structured provider supervision', () => { expect.objectContaining({ command: '/opt/codex', args: ['app-server', '--flag'], - cwd: '/work/repo' + cwd: '/work/repo', + ownerPid: process.pid, + stdinEndGraceMs: PROVIDER_STDIN_END_GRACE_MS, + sigtermGraceMs: PROVIDER_SIGTERM_GRACE_MS }) ) expect( @@ -38,7 +43,7 @@ describe('structured provider supervision', () => { ) expect(POSIX_PROVIDER_SUPERVISOR_SCRIPT).toContain('delete childEnv.ELECTRON_RUN_AS_NODE') expect(spec.env.ELECTRON_RUN_AS_NODE).toBe('1') - expect(POSIX_PROVIDER_SUPERVISOR_SCRIPT).toContain('process.ppid !== originalParent') + expect(POSIX_PROVIDER_SUPERVISOR_SCRIPT).toContain('process.ppid !== spec.ownerPid') expect(POSIX_PROVIDER_SUPERVISOR_SCRIPT).toContain( "process.stdin.once('close', scheduleOwnerShutdown)" ) @@ -49,13 +54,26 @@ describe('structured provider supervision', () => { expect(POSIX_PROVIDER_SUPERVISOR_SCRIPT).not.toContain('process.ppid === 1') }) + it('refuses a grace longer than recovery waits before SIGKILL', () => { + const stdinEnd = (stdinEndGraceMs: number) => () => + supervisedPosixLaunch(launch, {}, { stdinEndGraceMs }) + const sigterm = (sigtermGraceMs: number) => () => + supervisedPosixLaunch(launch, {}, { sigtermGraceMs }) + + expect(stdinEnd(PROVIDER_STDIN_END_GRACE_MS)).not.toThrow() + expect(stdinEnd(PROVIDER_STDIN_END_GRACE_MS + 1)).toThrow(RangeError) + expect(sigterm(PROVIDER_SIGTERM_GRACE_MS)).not.toThrow() + expect(sigterm(PROVIDER_SIGTERM_GRACE_MS + 1)).toThrow(RangeError) + }) + it('uses direct provider spawning on Windows because the job owns the tree', () => { expect(createProviderSpawnSpec(launch, { PATH: '/bin' }, 'win32')).toEqual({ program: '/opt/codex', args: ['app-server', '--flag'], env: { PATH: '/bin' }, cwd: '/work/repo', - detached: false + detached: false, + supervised: false }) }) }) diff --git a/src/main/codex/codex-app-server-posix-supervisor.ts b/src/main/codex/codex-app-server-posix-supervisor.ts index f83ba6f374b..21dd56aeb5b 100644 --- a/src/main/codex/codex-app-server-posix-supervisor.ts +++ b/src/main/codex/codex-app-server-posix-supervisor.ts @@ -1,9 +1,33 @@ import type { CodexAppServerLaunch } from './codex-app-server-connection' +/** Time the provider gets to exit on its own after its stdin ends, before SIGTERM. */ +export const PROVIDER_STDIN_END_GRACE_MS = 1_000 +/** Time the provider group gets to flush and exit after SIGTERM, before SIGKILL. */ +export const PROVIDER_SIGTERM_GRACE_MS = 3_000 +/** + * How long the supervisor waits for a SIGKILLed group to disappear. A killed process never runs + * again, so this only covers the kernel finishing the kill; waiting forever could hang close or + * recovery on a process stuck in the kernel, such as one blocked on a hung network drive. + */ +export const PROVIDER_GROUP_REAP_TIMEOUT_MS = 1_500 +/** + * Longest a supervisor can take to stop once asked (stdin end, grace, SIGTERM, grace, SIGKILL, + * reap); a SIGKILL sooner can orphan its group. The graces are also the largest a spec may carry. + */ +export const PROVIDER_SUPERVISOR_MAX_STOP_MS = + PROVIDER_STDIN_END_GRACE_MS + PROVIDER_SIGTERM_GRACE_MS + PROVIDER_GROUP_REAP_TIMEOUT_MS + /** Inline supervisor source kept dependency-free for the spawned Node child. */ export const POSIX_PROVIDER_SUPERVISOR_SCRIPT = ` const { spawn } = require('node:child_process') const spec = JSON.parse(Buffer.from(process.env.ORCA_PROVIDER_SUPERVISOR_SPEC, 'base64').toString()) +// A detached supervisor is reparented when its owner exits. The new parent may +// be PID 1 or a platform subreaper, so any other parent means no live owner. +const ownerGone = () => process.ppid !== spec.ownerPid +// Registered before the spawn, so a stop that lands while the provider starts still reaps it. +for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => stopProviderGroup(signal)) +// Orca can die before this runs; spawning then would start a provider nothing watches. +if (ownerGone()) process.exit(1) const childEnv = { ...process.env } delete childEnv.ORCA_PROVIDER_SUPERVISOR_SPEC delete childEnv.ELECTRON_RUN_AS_NODE @@ -13,7 +37,6 @@ const child = spawn(spec.command, spec.args, { stdio: ['pipe', 'pipe', 'pipe'], detached: true }) -const originalParent = process.ppid let timer let ownerShutdownTimer let settling = false @@ -26,29 +49,47 @@ const providerGroupExists = () => { return Boolean(error && error.code !== 'ESRCH') } } -const reapOwnedProviderGroup = async () => { - if (!child.pid) return false - try { process.kill(-child.pid, 'SIGKILL') } catch (error) { - if (error && error.code !== 'ESRCH') return false - } - const deadline = Date.now() + 1500 +const waitForProviderGroupExit = async (timeoutMs) => { + const deadline = Date.now() + timeoutMs while (providerGroupExists()) { if (Date.now() >= deadline) return false await new Promise((resolve) => setTimeout(resolve, 25)) } return true } -const terminateOwnedGroup = () => { +const reapOwnedProviderGroup = async () => { + if (!child.pid) return false + try { process.kill(-child.pid, 'SIGKILL') } catch (error) { + if (error && error.code !== 'ESRCH') return false + } + return waitForProviderGroupExit(${PROVIDER_GROUP_REAP_TIMEOUT_MS}) +} +const finishWithProviderOutcome = (code, signal) => { + if (!signal) return process.exit(code ?? 1) + // Re-raise with the default action; this supervisor's own handler would swallow it. + process.removeAllListeners(signal) + process.kill(process.pid, signal) +} +// Every stop is the same: SIGTERM the group, SIGKILL it after its grace, and exit only once it is +// gone. Whoever stops this pid judges the provider by it, so a dead supervisor means a dead group. +const stopProviderGroup = (receivedSignal) => { if (settling) return settling = true clearInterval(timer) - void reapOwnedProviderGroup().then((reaped) => process.exit(reaped ? 137 : 1)) + if (ownerShutdownTimer) clearTimeout(ownerShutdownTimer) + try { process.kill(-child.pid, 'SIGTERM') } catch {} + void waitForProviderGroupExit(spec.sigtermGraceMs) + .then((exited) => exited || reapOwnedProviderGroup()) + .then((reaped) => { + if (!reaped) return process.exit(1) + finishWithProviderOutcome(137, receivedSignal) + }) } const scheduleOwnerShutdown = () => { if (settling || ownerShutdownTimer) return // A normal close ends the provider's stdin first; allow it to flush and // exit before forcing the group, while still bounding an orphaned child. - ownerShutdownTimer = setTimeout(terminateOwnedGroup, 1250) + ownerShutdownTimer = setTimeout(() => stopProviderGroup(null), spec.stdinEndGraceMs) ownerShutdownTimer.unref() } process.stdin.once('end', scheduleOwnerShutdown) @@ -56,10 +97,9 @@ process.stdin.once('close', scheduleOwnerShutdown) process.stdin.pipe(child.stdin) child.stdout.pipe(process.stdout) child.stderr.pipe(process.stderr) -for (const stream of [process.stdin, child.stdin, child.stdout, child.stderr]) stream.on('error', () => {}) -const finishWithProviderOutcome = (code, signal) => { - if (!signal) return process.exit(code ?? 1) - process.kill(process.pid, signal) +// A dead owner's stdout pipe raises EPIPE; unhandled, it would end this pid before the group. +for (const stream of [process.stdin, process.stdout, process.stderr, child.stdin, child.stdout, child.stderr]) { + stream.on('error', () => {}) } const reapProviderExit = async (code, signal) => { if (settling) return @@ -70,33 +110,54 @@ const reapProviderExit = async (code, signal) => { finishWithProviderOutcome(code, signal) } timer = setInterval(() => { - // A detached supervisor is reparented when its owner exits. The new parent - // may be PID 1 or a platform subreaper, so any parent change is proof that - // this process group no longer has a live Orca owner. - if (process.ppid !== originalParent) { - terminateOwnedGroup() - } + if (ownerGone()) stopProviderGroup(null) }, 100) timer.unref() -child.once('error', () => { +child.once('error', (error) => { clearInterval(timer) - process.exit(127) + // The owner sees only this pid's exit; stderr is where a missing provider binary can say so. + process.stderr.write(String(error && error.message) + '\\n', () => process.exit(127)) }) child.once('exit', (code, signal) => { void reapProviderExit(code, signal) }) ` +export type ProviderSupervisorOptions = { + cwd?: string + /** The process the supervisor serves; it must be the supervisor's parent. */ + ownerPid?: number + stdinEndGraceMs?: number + sigtermGraceMs?: number +} + +// A longer grace than the max stop allows would let recovery or close SIGKILL mid-stop. +function assertGraceWithin(name: string, graceMs: number, maxMs: number): void { + if (!(graceMs >= 0 && graceMs <= maxMs)) { + throw new RangeError(`Provider supervisor ${name} grace ${graceMs} ms is outside 0-${maxMs} ms`) + } +} + export function supervisedPosixLaunch( launch: CodexAppServerLaunch, childEnv: NodeJS.ProcessEnv, - cwd = launch.cwd ?? process.cwd() + { + cwd = launch.cwd ?? process.cwd(), + ownerPid = process.pid, + stdinEndGraceMs = PROVIDER_STDIN_END_GRACE_MS, + sigtermGraceMs = PROVIDER_SIGTERM_GRACE_MS + }: ProviderSupervisorOptions = {} ): { command: string; args: string[]; env: NodeJS.ProcessEnv } { + assertGraceWithin('stdin-end', stdinEndGraceMs, PROVIDER_STDIN_END_GRACE_MS) + assertGraceWithin('SIGTERM', sigtermGraceMs, PROVIDER_SIGTERM_GRACE_MS) const supervisorSpec = Buffer.from( JSON.stringify({ command: launch.command, args: launch.args, - cwd + cwd, + ownerPid, + stdinEndGraceMs, + sigtermGraceMs }) ).toString('base64') return { @@ -116,13 +177,22 @@ export function createProviderSpawnSpec( launch: CodexAppServerLaunch, childEnv: NodeJS.ProcessEnv, platform: NodeJS.Platform -): { program: string; args: string[]; env: NodeJS.ProcessEnv; cwd: string; detached: boolean } { - const supervised = platform === 'win32' ? null : supervisedPosixLaunch(launch, childEnv) +): { + program: string + args: string[] + env: NodeJS.ProcessEnv + cwd: string + detached: boolean + /** The child is the supervisor, whose SIGTERM stops the provider and then itself. */ + supervised: boolean +} { + const supervisor = platform === 'win32' ? null : supervisedPosixLaunch(launch, childEnv) return { - program: supervised?.command ?? launch.command, - args: supervised?.args ?? launch.args, - env: supervised?.env ?? childEnv, + program: supervisor?.command ?? launch.command, + args: supervisor?.args ?? launch.args, + env: supervisor?.env ?? childEnv, cwd: launch.cwd ?? process.cwd(), - detached: platform !== 'win32' + detached: platform !== 'win32', + supervised: supervisor !== null } } diff --git a/src/main/codex/codex-app-server-process-teardown.test.ts b/src/main/codex/codex-app-server-process-teardown.test.ts index cec8f91d081..295d780718d 100644 --- a/src/main/codex/codex-app-server-process-teardown.test.ts +++ b/src/main/codex/codex-app-server-process-teardown.test.ts @@ -26,7 +26,7 @@ describe('terminateCodexAppServerProcessTree', () => { const release = Promise.withResolvers() const terminateWindowsTree = vi.fn(() => release.promise) - const teardown = terminateCodexAppServerProcessTree(target, undefined, { + const teardown = terminateCodexAppServerProcessTree(target, { platform: 'win32', terminateWindowsTree }) @@ -38,52 +38,12 @@ describe('terminateCodexAppServerProcessTree', () => { expect(target.kill).toHaveBeenCalledWith('SIGKILL') }) - it('kills exact Linux spawn-token PIDs before the recorded wrapper', async () => { - const target = child() - const findSpawnTokenProcesses = vi - .fn<() => Promise>() - .mockResolvedValueOnce([1234, 2345, 3456]) - .mockResolvedValueOnce([1234]) - .mockResolvedValueOnce([1234]) - const signalPid = vi.fn() - - await expect( - terminateCodexAppServerProcessTree(target, 'spawn-1', { - platform: 'linux', - findSpawnTokenProcesses, - signalPid, - isPidPresent: () => false, - wait: async () => undefined - }) - ).resolves.toBe(true) - - expect(signalPid.mock.calls).toEqual([ - [2345, 'SIGKILL'], - [3456, 'SIGKILL'] - ]) - expect(target.kill).toHaveBeenCalledTimes(1) - expect(target.kill).toHaveBeenCalledWith('SIGKILL') - }) - - it('keeps the wrapper reachable when Linux cannot prove descendant exit', async () => { - const target = child() - - await expect( - terminateCodexAppServerProcessTree(target, 'spawn-1', { - platform: 'linux', - findSpawnTokenProcesses: async () => null - }) - ).resolves.toBe(false) - - expect(target.kill).not.toHaveBeenCalled() - }) - it('waits for an owned POSIX snapshot before killing the wrapper', async () => { const target = child() const snapshot = { rootPgid: 1234, descendants: [], capturedAtMs: 1 } const release = Promise.withResolvers() - const teardown = terminateCodexAppServerProcessTree(target, undefined, { + const teardown = terminateCodexAppServerProcessTree(target, { platform: 'darwin', captureDescendants: async () => snapshot, terminateDescendants: () => release.promise @@ -102,7 +62,7 @@ describe('terminateCodexAppServerProcessTree', () => { const signalProcessGroup = vi.fn() await expect( - terminateCodexAppServerProcessTree(target, undefined, { + terminateCodexAppServerProcessTree(target, { platform: 'darwin', dedicatedProcessGroup: true, captureDescendants, @@ -119,7 +79,7 @@ describe('terminateCodexAppServerProcessTree', () => { const target = child() await expect( - terminateCodexAppServerProcessTree(target, undefined, { + terminateCodexAppServerProcessTree(target, { platform: 'linux', dedicatedProcessGroup: true, signalProcessGroup: () => { @@ -142,7 +102,7 @@ describe('terminateCodexAppServerProcessTree', () => { const target = { pid: UNREACHABLE_PGID, kill: vi.fn(() => true) as ChildProcess['kill'] } await expect( - terminateCodexAppServerProcessTree(target, undefined, { + terminateCodexAppServerProcessTree(target, { platform: 'darwin', captureDescendants: async () => ({ rootPgid: UNREACHABLE_PGID, @@ -162,7 +122,7 @@ describe('terminateCodexAppServerProcessTree', () => { const signalProcessGroup = vi.fn() await expect( - terminateCodexAppServerProcessTree(target, undefined, { + terminateCodexAppServerProcessTree(target, { platform: 'darwin', captureDescendants: async () => ({ rootPgid: 1234, descendants: [], capturedAtMs: 1 }), terminateDescendants: async () => true, @@ -191,7 +151,7 @@ describe('terminateCodexAppServerProcessTree', () => { const results = await Promise.all( targets.map((target) => - terminateCodexAppServerProcessTree(target, undefined, { + terminateCodexAppServerProcessTree(target, { platform: 'linux', dedicatedProcessGroup: true, captureDescendants, diff --git a/src/main/codex/codex-app-server-process-teardown.ts b/src/main/codex/codex-app-server-process-teardown.ts index 5a9c6e3574b..10e625bef7e 100644 --- a/src/main/codex/codex-app-server-process-teardown.ts +++ b/src/main/codex/codex-app-server-process-teardown.ts @@ -2,11 +2,8 @@ import type { ChildProcessHandle } from '../../shared/child-process/run-process' import { captureDescendantSnapshot, type DescendantSnapshot } from '../pty-descendant-termination' import { terminateDescendantSnapshotAndWait } from '../pty-descendant-exit-verification' import { terminateWindowsProcessTree } from '../windows-process-tree-kill' -import { findAgentSessionSpawnTokenProcesses } from '../runtime/agent-session-spawn-token-process-scan' import { recordSelfInitiatedTreeKill } from '../crash-reporting/self-initiated-tree-kill-log' -const TOKEN_PROCESS_EXIT_TIMEOUT_MS = 3_500 -const TOKEN_PROCESS_POLL_MS = 25 const activeTeardowns = new WeakMap>() type TeardownChild = Pick @@ -14,16 +11,10 @@ type TeardownChild = Pick export type CodexAppServerProcessTeardownDeps = { platform?: NodeJS.Platform dedicatedProcessGroup?: boolean - /** Diagnostic/recovery injection only; never used by the primary teardown. */ - findSpawnTokenProcesses?: (spawnToken: string) => Promise captureDescendants?: (rootPid: number) => Promise terminateDescendants?: (snapshot: DescendantSnapshot) => Promise terminateWindowsTree?: (rootPid: number, deps?: { site?: string }) => Promise - signalPid?: (pid: number, signal: NodeJS.Signals) => void signalProcessGroup?: (pgid: number, signal: NodeJS.Signals) => void - isPidPresent?: (pid: number) => boolean - wait?: (ms: number) => Promise - now?: () => number } function terminateDedicatedPosixGroup( @@ -47,70 +38,11 @@ function terminateDedicatedPosixGroup( return true } -function sendSignal(pid: number, signal: NodeJS.Signals): void { - try { - process.kill(pid, signal) - } catch { - // An already-gone exact PID is the desired outcome. - } -} - -function isPidPresent(pid: number): boolean { - try { - process.kill(pid, 0) - return true - } catch (error) { - return (error as NodeJS.ErrnoException).code !== 'ESRCH' - } -} - -async function diagnosticTokenFallback( - rootPid: number, - spawnToken: string, - deps: CodexAppServerProcessTeardownDeps -): Promise { - const find = deps.findSpawnTokenProcesses ?? findAgentSessionSpawnTokenProcesses - const signal = deps.signalPid ?? sendSignal - const pidPresent = deps.isPidPresent ?? isPidPresent - const delay = - deps.wait ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))) - const now = deps.now ?? Date.now - const deadline = now() + TOKEN_PROCESS_EXIT_TIMEOUT_MS - const signalled = new Set() - while (now() < deadline) { - const pids = await find(spawnToken).catch(() => null) - if (pids === null) { - return false - } - for (const pid of pids.filter((candidate) => candidate !== rootPid)) { - signalled.add(pid) - signal(pid, 'SIGKILL') - } - if ([...signalled].every((pid) => !pidPresent(pid))) { - return true - } - await delay(TOKEN_PROCESS_POLL_MS) - } - return false -} - async function terminatePosixTree( child: TeardownChild, rootPid: number, - _spawnToken: string | undefined, deps: CodexAppServerProcessTeardownDeps ): Promise { - // Kept only for explicit recovery callers/tests. Production always follows - // the dedicated process-group path below; token enumeration is evidence, - // never the owner of orphan-reaping decisions. - if (_spawnToken && deps.findSpawnTokenProcesses) { - const reaped = await diagnosticTokenFallback(rootPid, _spawnToken, deps) - if (reaped) { - child.kill('SIGKILL') - return true - } - return false - } child.kill('SIGSTOP') const capture = deps.captureDescendants ?? captureDescendantSnapshot const snapshot = await capture(rootPid).catch(() => null) @@ -158,7 +90,6 @@ async function terminatePosixTree( /** Stops every process owned by one app-server launch before releasing its wrapper. */ async function terminateOnce( child: TeardownChild, - spawnToken: string | undefined, deps: CodexAppServerProcessTeardownDeps ): Promise { const rootPid = child.pid @@ -176,12 +107,11 @@ async function terminateOnce( if (deps.dedicatedProcessGroup) { return terminateDedicatedPosixGroup(rootPid, deps) } - return terminatePosixTree(child, rootPid, spawnToken, deps) + return terminatePosixTree(child, rootPid, deps) } export function terminateCodexAppServerProcessTree( child: TeardownChild, - spawnToken?: string, deps: CodexAppServerProcessTeardownDeps = {} ): Promise { const key = child as object @@ -189,7 +119,7 @@ export function terminateCodexAppServerProcessTree( if (active) { return active } - const attempt = terminateOnce(child, spawnToken, deps).catch(() => false) + const attempt = terminateOnce(child, deps).catch(() => false) activeTeardowns.set(key, attempt) void attempt.then(() => { if (activeTeardowns.get(key) === attempt) { diff --git a/src/main/codex/codex-background-task-frames.ts b/src/main/codex/codex-background-task-frames.ts index 65f51846c24..029cc7b50c7 100644 --- a/src/main/codex/codex-background-task-frames.ts +++ b/src/main/codex/codex-background-task-frames.ts @@ -7,8 +7,7 @@ import { import { codexChildTurnState } from './codex-subagent-executions' import { readRecord } from './codex-item-field-readers' import { readCodexThreadItem } from './codex-structured-item-translation' -import { readCodexProviderVerdict } from './codex-structured-journal-provider-verdicts' -import { readCodexTurnId } from './codex-structured-thread-facts' +import { readCodexErrorWillRetry, readCodexTurnId } from './codex-structured-thread-facts' export type CodexBackgroundTaskFrame = | { @@ -43,15 +42,18 @@ export type CodexBackgroundTaskEvent = { /** * The two ways Codex ends a child's turn without `turn/completed`. An `error` it will not retry is - * that turn's own end: the verdict the transcript settles the same turn on. A closed thread ran - * its last turn, and Codex never said how it went. A `systemError` status is neither: Codex raises - * it for errors that leave the turn running too (a refused steer), and a turn one ends also - * carries the `error`. + * that turn's own end: for a child, `turn/completed` may never follow, and without this end the + * child's lifecycle row latches on `working` for the life of the session. A closed thread ran its + * last turn, and Codex never said how it went. + * + * #23682 dropped the equivalent reading from the primary journal path, where Codex's own failed + * `turn/completed` always follows within ~32 ms and carries the duration the error lacks. A child + * has no such guarantee, which is why this path still settles on the error. */ function readCodexChildTurnEnding( event: CodexBackgroundTaskEvent ): CodexBackgroundTaskFrame | null { - if (readCodexProviderVerdict(event.method, event.params) === 'turn-failed') { + if (event.method === 'error' && !readCodexErrorWillRetry(event.params)) { const turnId = readCodexTurnId(event.params) return { kind: 'turn-ended', threadId: event.threadId, turnId, state: 'failed' } } diff --git a/src/main/codex/codex-structured-journal-provider-verdicts.ts b/src/main/codex/codex-structured-journal-provider-verdicts.ts deleted file mode 100644 index c8685dfedab..00000000000 --- a/src/main/codex/codex-structured-journal-provider-verdicts.ts +++ /dev/null @@ -1,40 +0,0 @@ -// What Codex's own verdict frames mean beyond the row they print. -// -// Both are decoration to the transcript and load-bearing to the session's state, -// which is why they are read here rather than left to the generic-frame fallback. - -import { codexThreadStoppedRunning, readCodexErrorWillRetry } from './codex-structured-thread-facts' - -export type CodexProviderVerdict = - /** - * Codex ended this turn with a fault. - * - * The app server emits `error` ONLY for a failure that affects turn status, and - * hardcodes `willRetry: false` there; a stream error it is about to retry carries - * `willRetry: true` and ends nothing. `turn/completed` may never follow, so this - * frame is the turn's only end — without it the running lifecycle row is a latch - * nothing re-derives and the chat reads working for the life of the session. - */ - | 'turn-failed' - /** - * Codex reports the thread is no longer running. - * - * This settles no OPEN turn: the app server clears `running` on every error, - * including the ones it says do not affect turn status, so a turn still open - * here is still running and `turn/completed` is its end. What it does settle is - * a send whose dispatch was never answered — a timed-out dispatch is recorded - * as unverified delivery, reads as work still owed, and nothing else in a live - * session re-derives it. - */ - | 'thread-stopped-running' - | null - -export function readCodexProviderVerdict(method: string, params: unknown): CodexProviderVerdict { - if (method === 'error') { - return readCodexErrorWillRetry(params) ? null : 'turn-failed' - } - if (method === 'thread/status/changed') { - return codexThreadStoppedRunning(params) ? 'thread-stopped-running' : null - } - return null -} diff --git a/src/main/codex/codex-structured-journal-settlement.ts b/src/main/codex/codex-structured-journal-settlement.ts index e2164b9498d..39d52e6b047 100644 --- a/src/main/codex/codex-structured-journal-settlement.ts +++ b/src/main/codex/codex-structured-journal-settlement.ts @@ -174,82 +174,6 @@ export function settleCodexJournalTurn(input: { return ADMITTED } -/** Settle streamed items whose terminal notification was rejected as oversized. */ -export function settleCodexOversizedNotification(input: { - sessionId: string - threadId: string - method: string - sink: StructuredAgentSessionEventSink - streams: CodexStructuredItemStreams - activeItems: Map - linkageFor: CodexRowLinkage -}): StructuredAgentSessionSinkAdmission { - const itemType = oversizedStreamItemType(input.method) - if (!itemType) { - return ADMITTED - } - const mutations: JournalLifecycleMutationInput[] = [] - const activeItemsToForget: { key: string; threadId: string; itemId: string }[] = [] - for (const [key, active] of input.activeItems) { - if (active.threadId !== input.threadId || active.item.type !== itemType) { - continue - } - const streamed = input.streams.snapshot(active.threadId, active.item.id) - const translated = streamed - ? codexStreamingJournalItem(active.item, streamed.text) - : codexJournalItem(active.item) - const body = interruptedBody(translated.body) - if (body) { - mutations.push(settledRow(input.linkageFor, active, body)) - } - activeItemsToForget.push({ key, threadId: active.threadId, itemId: active.item.id }) - } - if (mutations.length === 0) { - return ADMITTED - } - const admission = appendCodexLifecycleMutations( - input.sink, - `oversized-notification:${input.sessionId}:${input.threadId}:${input.method}`, - mutations - ) - if (!admission.accepted) { - return admission - } - for (const active of activeItemsToForget) { - input.streams.forget(active.threadId, active.itemId) - input.activeItems.delete(active.key) - } - return ADMITTED -} - -function oversizedStreamItemType(method: string): CodexThreadItem['type'] | null { - if (method === 'item/agentMessage/delta') { - return 'agentMessage' - } - if (method === 'item/plan/delta') { - return 'plan' - } - if ( - method === 'command/exec/outputDelta' || - method === 'process/outputDelta' || - method === 'item/commandExecution/outputDelta' || - method === 'item/commandExecution/terminalInteraction' - ) { - return 'commandExecution' - } - if (method === 'item/fileChange/outputDelta' || method === 'item/fileChange/patchUpdated') { - return 'fileChange' - } - if ( - method === 'item/reasoning/summaryTextDelta' || - method === 'item/reasoning/summaryPartAdded' || - method === 'item/reasoning/textDelta' - ) { - return 'reasoning' - } - return null -} - /** A settled item or prompt, naming its producer: the settlement can be the row's first write. */ function settledRow( linkageFor: CodexRowLinkage, diff --git a/src/main/codex/codex-structured-journal-translation-frames.ts b/src/main/codex/codex-structured-journal-translation-frames.ts deleted file mode 100644 index f94c10bfe66..00000000000 --- a/src/main/codex/codex-structured-journal-translation-frames.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * The translator's provider-frame arms. - * - * Each returns null for a frame it does not own, which is the translator's - * signal to keep looking. Split out so the translator reads as routing rather - * than as the shape checks each arm performs. - */ - -import type { CodexStructuredSessionEvent } from './codex-structured-session-adapter' -import type { CodexJournalItems } from './codex-structured-journal-items' -import type { CodexJournalTranslationAdmission } from './codex-structured-journal-contracts' -import { settleCodexOversizedNotification } from './codex-structured-journal-settlement' -import { - readCodexJournalRecord, - readCodexJournalString -} from './codex-structured-journal-translation-values' - -type OversizedInput = Parameters[0] - -/** A notification the transport refused to carry whole: settle whatever it - * opened rather than leaving the item mid-flight. */ -export function settleCodexOversizedNotificationFrame(input: { - sessionId: string - threadId: string - kind: string - payload: unknown - sink: OversizedInput['sink'] - streams: OversizedInput['streams'] - activeItems: OversizedInput['activeItems'] - linkageFor: OversizedInput['linkageFor'] -}): CodexJournalTranslationAdmission | null { - if (input.kind !== 'frame:oversized-notification') { - return null - } - const method = readCodexJournalString(readCodexJournalRecord(input.payload), 'method') - return method - ? settleCodexOversizedNotification({ - sessionId: input.sessionId, - threadId: input.threadId, - method, - sink: input.sink, - streams: input.streams, - activeItems: input.activeItems, - linkageFor: input.linkageFor - }) - : null -} - -export function createCodexOversizedNotificationSettler( - deps: { sink: OversizedInput['sink']; linkageFor: OversizedInput['linkageFor'] }, - items: Pick -) { - return settleOversizedNotification - - /** Settles the item a notification the transport refused to carry left - * mid-flight; null when the frame is not one. */ - function settleOversizedNotification( - event: Extract - ): CodexJournalTranslationAdmission | null { - return settleCodexOversizedNotificationFrame({ - ...event, - sink: deps.sink, - streams: items.streams, - activeItems: items.activeItems, - linkageFor: deps.linkageFor - }) - } -} diff --git a/src/main/codex/codex-structured-journal-translation-streams.test.ts b/src/main/codex/codex-structured-journal-translation-streams.test.ts index 8a924a9d78a..ce6fd220132 100644 --- a/src/main/codex/codex-structured-journal-translation-streams.test.ts +++ b/src/main/codex/codex-structured-journal-translation-streams.test.ts @@ -215,55 +215,6 @@ describe('codex journal translation', () => { ).toEqual(['notification:future/notification', 'request:future/request', 'frame:unclassified']) }) - it('terminalizes the active streamed item when an oversized notification is rejected', () => { - const { translator, tap } = translatorWith() - translator.handle(TURN_STARTED) - translator.handle( - notification('item/started', { - item: { - type: 'commandExecution', - id: 'exec-oversized', - command: 'run', - status: 'inProgress' - } - }) - ) - const admission = translator.handle({ - type: 'provider-frame', - sessionId: SESSION_ID, - threadId: THREAD_ID, - kind: 'frame:oversized-notification', - payload: { - reason: 'record-too-large', - observedBytes: 20 * 1024 * 1024, - maxBytes: 16 * 1024 * 1024, - classification: 'notification', - method: 'item/commandExecution/outputDelta' - } - }) - - expect(admission).toEqual({ accepted: true }) - expect(tap.rows).toEqual([ - expect.objectContaining({ - body: expect.objectContaining({ kind: 'tool-call', state: 'running' }) - }), - expect.objectContaining({ - body: expect.objectContaining({ kind: 'tool-call', state: 'failed' }) - }), - expect.objectContaining({ - body: expect.objectContaining({ - kind: 'status', - providerFrame: expect.objectContaining({ kind: 'frame:oversized-notification' }) - }) - }) - ]) - const diagnostic = tap.rows[2]?.body - expect( - diagnostic?.kind === 'status' ? diagnostic.providerFrame?.payload.byteLength : 0 - ).toBeGreaterThan(0) - expect(JSON.stringify(diagnostic)).toContain('record-too-large') - }) - it('admits suppressed diagnostics before settling a completed turn', () => { const tap = recorder() let rejectSuppression = true 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 2c958858816..3116a329c97 100644 --- a/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts +++ b/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts @@ -140,6 +140,8 @@ export class CodexJournalTurnBoundaries { return admission } + /** Codex ends every turn with exactly one `turn/completed`, a failed one after + * its `error` frame included, so this is the only live end. */ complete(event: TurnBoundaryEvent): CodexJournalTranslationAdmission { const suppressionAdmission = this.deps.flushSuppression() if (!suppressionAdmission.accepted) { @@ -154,58 +156,14 @@ export class CodexJournalTurnBoundaries { // turn boundary is no evidence contact was lost. Only `settleSession` may // write `unverifiable`. const status = readCodexTurnStatus(event.params) - return this.end(event, turnId, { - state: codexTurnLifecycleState(status), - outcome: codexTurnOutcome(status), - completedAt: this.receiptTime(event), - durationMs: readCodexTurnDurationMs(event.params) - }) - } - - /** - * Settles the turn a terminal `error` names. - * - * Codex reports a fault that ended a turn as an `error` notification carrying - * that turn's id, and `turn/completed` may never follow it — the app server - * marks the thread not-running off the error alone. Without this the running - * lifecycle row is a latch nothing re-derives, and the chat reads "Working" - * for the life of the session. A retrying stream error is NOT a turn end and - * never reaches here. - */ - fail(event: TurnBoundaryEvent): CodexJournalTranslationAdmission { - const suppressionAdmission = this.deps.flushSuppression() - if (!suppressionAdmission.accepted) { - return suppressionAdmission - } - const turnId = readCodexTurnId(event.params) ?? this.deps.activeTurns.current(event.threadId) - // An error ends only a turn this host saw open; its `turn/completed` settles any other. - if (!turnId || !this.deps.activeTurns.isActive(event.threadId, turnId)) { - return CODEX_JOURNAL_ADMITTED - } - return this.end(event, turnId, { - state: 'completed', - outcome: 'failure', - completedAt: this.receiptTime(event) - }) - } - - /** - * A turn's first terminal settlement is final. Codex follows a turn-ending - * `error` with a failed `turn/completed` for the same turn, and by then the - * start and attributed send this row carries are forgotten, so a second end - * could only overwrite the record with less. - */ - private end( - event: TurnBoundaryEvent, - turnId: string, - terminal: TurnTerminal - ): CodexJournalTranslationAdmission { - if (this.recentTurns.has(event.threadId, turnId)) { - return CODEX_JOURNAL_ADMITTED - } const turnLifecycle = event.threadId === this.deps.primaryThreadId() - ? this.settled(event.threadId, turnId, terminal) + ? 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( 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 6fc1e88d184..62aa5f828a1 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 @@ -346,7 +346,7 @@ describe('codex turn lifecycle rows', () => { const lifecycle = reduced(tap.rows).find((row) => row.key === LIFECYCLE_KEY) expect(lifecycle?.body).toMatchObject({ kind: 'turn', - state: 'interrupted', + state: 'completed', outcome: 'failure', requestedAt: 900 }) @@ -381,17 +381,17 @@ describe('codex turn lifecycle rows', () => { }) // `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. + // inProgress`. Only `interrupted` is a stop; every other end completed the + // turn, and `outcome` says how. A status this build cannot place stays unknown + // rather than borrowing a verdict. it.each([ - ['interrupted', 'cancellation'], - ['failed', 'failure'], - ['cancelled', undefined], - ['inProgress', undefined] + ['interrupted', 'interrupted', 'cancellation'], + ['failed', 'completed', 'failure'], + ['someFutureStatus', 'completed', undefined], + ['inProgress', 'completed', undefined] ] as const)( - 'maps a %s turn status to an interrupted lifecycle with outcome %s', - (status, outcome) => { + 'maps a %s turn status to a %s lifecycle with outcome %s', + (status, state, outcome) => { const tap = recorder() const translator = translatorFor(tap) @@ -405,7 +405,7 @@ describe('codex turn lifecycle rows', () => { body: { kind: 'turn', turnId: TURN_ID, - state: 'interrupted', + state, ...(outcome ? { outcome } : {}), userItemId: USER_ITEM_ID, startedAt: 1_000, @@ -512,6 +512,21 @@ describe('codex turn lifecycle rows', () => { completedAt: 1_700_000_101, items: [] }, + { + id: 'turn-failed', + status: 'failed', + startedAt: 1_700_000_150, + completedAt: 1_700_000_152, + durationMs: 2_400, + items: [] + }, + { + id: 'turn-unplaced', + status: 'someFutureStatus', + startedAt: 1_700_000_170, + completedAt: 1_700_000_171, + items: [] + }, { id: 'turn-open', status: 'inProgress', startedAt: 1_700_000_200, items: [] }, { id: 'turn-untimed', status: 'completed', items: [] } ] @@ -544,6 +559,32 @@ describe('codex turn lifecycle rows', () => { startedAt: 1_700_000_100_000, completedAt: 1_700_000_101_000 } + }, + { + // The same shape a live failed completion writes. + key: 'legacy:codex:session-1:turn-lifecycle%3Aturn-failed', + body: { + kind: 'turn', + turnId: 'turn-failed', + state: 'completed', + outcome: 'failure', + userItemId: 'codex:thread-abc:turn-failed:0', + startedAt: 1_700_000_150_000, + completedAt: 1_700_000_152_000, + durationMs: 2_400 + } + }, + { + // Ended, but not a status this build can place: no verdict, never a clean finish. + key: 'legacy:codex:session-1:turn-lifecycle%3Aturn-unplaced', + body: { + kind: 'turn', + turnId: 'turn-unplaced', + state: 'completed', + userItemId: 'codex:thread-abc:turn-unplaced:0', + startedAt: 1_700_000_170_000, + completedAt: 1_700_000_171_000 + } } ]) expect(tap.tombstones).toEqual([]) diff --git a/src/main/codex/codex-structured-journal-translation-turn-settles-once.test.ts b/src/main/codex/codex-structured-journal-translation-turn-settles-once.test.ts index a5c02bd3bfe..77638a7d1ea 100644 --- a/src/main/codex/codex-structured-journal-translation-turn-settles-once.test.ts +++ b/src/main/codex/codex-structured-journal-translation-turn-settles-once.test.ts @@ -21,13 +21,28 @@ type Row = { key: string; body: AgentJournalItemBody } function recorder() { const rows: Row[] = [] + let refuseTerminal = 0 const sink: StructuredAgentSessionEventSink = { appendItem: (identity: AgentJournalItemIdentity, body) => rows.push({ key: agentJournalItemKey(identity), body }), + tryAppendItem: (identity: AgentJournalItemIdentity, body) => { + if (body.kind === 'turn' && body.state !== 'running' && refuseTerminal > 0) { + refuseTerminal -= 1 + return { accepted: false, reason: 'backpressure' } + } + rows.push({ key: agentJournalItemKey(identity), body }) + return { accepted: true } + }, appendTombstone: () => {}, publish: () => {} } - return { sink, rows } + return { + sink, + rows, + refuseNextTerminalWrite: () => { + refuseTerminal += 1 + } + } } function notification(method: string, params: unknown, observedAt: number) { @@ -50,11 +65,15 @@ function translator(tap: ReturnType) { }) } -/** Every terminal write for a turn, in order: what a reader could observe. */ +/** Every lifecycle write for a turn, in order: what a reader could observe. */ +function turnWrites(rows: readonly Row[], turnId: string) { + return rows.flatMap((row) => + row.body.kind === 'turn' && row.body.turnId === turnId ? [row.body] : [] + ) +} + function terminalWrites(rows: readonly Row[], turnId: string) { - return rows - .map((row) => row.body) - .filter((body) => body.kind === 'turn' && body.turnId === turnId && body.state !== 'running') + return turnWrites(rows, turnId).filter((body) => body.state !== 'running') } /** The body the journal reducer keeps for a turn's lifecycle row. */ @@ -109,8 +128,8 @@ function runFailedTurn(handle: (event: CodexStructuredSessionEvent) => unknown) ) } -describe('a Codex turn settles once', () => { - it('keeps the failure the error settled when the failed completion follows it', () => { +describe('a Codex turn ends once, on its completion', () => { + it('records the failed completion Codex sends after the error, with the start this host saw', () => { const tap = recorder() const codex = translator(tap) @@ -125,35 +144,105 @@ describe('a Codex turn settles once', () => { userItemId: agentJournalSubmissionKey(CLIENT_MESSAGE_ID), startedAt: 1_000, requestedAt: 900, - completedAt: 2_000 + completedAt: 2_100, + durationMs: 1_100 }) expect( tap.rows.filter((row) => row.body.kind === 'status' && row.body.tone === 'error') ).toHaveLength(1) }) - it('ignores a duplicate completion for a turn it already settled', () => { + it('records an exit between the error and the completion as interrupted, with no verdict', () => { + // Orca records only an end it observed. The exit is that end; the failure + // Codex would have named arrives only in the completion, which never came. const tap = recorder() const codex = translator(tap) + const events: CodexStructuredSessionEvent[] = [] + runFailedTurn((event) => events.push(event)) + + for (const event of events.slice(0, -1)) { + codex.handle(event) + } + codex.handle({ + type: 'ended', + sessionId: SESSION_ID, + reason: 'lost child', + cause: 'unexpected-exit', + fence: 1, + acquisitionGeneration: 'generation-1', + observedAt: 2_050 + }) + + expect(terminalWrites(tap.rows, TURN_ID)).toEqual([ + { + kind: 'turn', + turnId: TURN_ID, + state: 'interrupted', + userItemId: agentJournalSubmissionKey(CLIENT_MESSAGE_ID), + startedAt: 1_000, + requestedAt: 900, + completedAt: 2_050 + } + ]) + expect( + tap.rows.filter((row) => row.body.kind === 'status' && row.body.tone === 'error') + ).toHaveLength(1) + }) + + it('revises the still-open turn with a send Codex echoes between the error and the completion', () => { + // Codex records a failed turn's pending input after its `error` frame and + // before `turn/completed`, so the echo belongs to a turn that is still open. + const tap = recorder() + const codex = translator(tap) + const events: CodexStructuredSessionEvent[] = [] + runFailedTurn((event) => events.push(event)) + const [started, echo, reply, error, completion] = events + + for (const event of [started, reply, error, echo, completion]) { + if (event) { + codex.handle(event) + } + } + + expect(turnWrites(tap.rows, TURN_ID).map((body) => body.state)).toEqual([ + 'running', + 'running', + 'completed' + ]) + expect(settledRecord(tap.rows, TURN_ID)).toMatchObject({ + outcome: 'failure', + userItemId: agentJournalSubmissionKey(CLIENT_MESSAGE_ID), + requestedAt: 900, + startedAt: 1_000, + durationMs: 1_100 + }) + }) + + it('settles once when the sink refuses the completion and its retry lands', () => { + // A refused frame is Orca's only redelivery of a completion, and a refused + // end changes nothing, so the retry settles the turn exactly once. + const tap = recorder() + const codex = translator(tap) + const completion = notification( + 'turn/completed', + { turn: { id: TURN_ID, status: 'completed', durationMs: 900 } }, + 2_000 + ) codex.handle(notification('turn/started', { turn: { id: TURN_ID } }, 1_000)) - codex.handle( - notification( - 'turn/completed', - { turn: { id: TURN_ID, status: 'completed', durationMs: 900 } }, - 2_000 - ) - ) - codex.handle(notification('turn/completed', { turn: { id: TURN_ID, status: 'failed' } }, 3_000)) + tap.refuseNextTerminalWrite() + expect(codex.handle(completion)).toEqual({ accepted: false, reason: 'backpressure' }) + expect(codex.handle(completion)).toEqual({ accepted: true }) - expect(terminalWrites(tap.rows, TURN_ID)).toHaveLength(1) - expect(settledRecord(tap.rows, TURN_ID)).toMatchObject({ - state: 'completed', - outcome: 'success', - startedAt: 1_000, - completedAt: 2_000, - durationMs: 900 - }) + expect(terminalWrites(tap.rows, TURN_ID)).toEqual([ + expect.objectContaining({ + state: 'completed', + outcome: 'success', + startedAt: 1_000, + completedAt: 2_000, + durationMs: 900 + }) + ]) }) it('settles an ordinary turn exactly as before', () => { diff --git a/src/main/codex/codex-structured-journal-translation-turn-state.ts b/src/main/codex/codex-structured-journal-translation-turn-state.ts index 832f57f3096..4c1e739705d 100644 --- a/src/main/codex/codex-structured-journal-translation-turn-state.ts +++ b/src/main/codex/codex-structured-journal-translation-turn-state.ts @@ -57,12 +57,6 @@ export class CodexJournalActiveTurns { return [...(this.byThread.get(threadId) ?? [])].at(-1) ?? null } - /** Whether this turn is still open here. A terminal row already written carries - * the turn's start and duration, so a later end must not overwrite it. */ - isActive(threadId: string, turnId: string): boolean { - return this.byThread.get(threadId)?.has(turnId) === true - } - startedAt(threadId: string, turnId: string): number | undefined { return this.startedAtByTurn.get(this.turnKey(threadId, turnId)) } @@ -167,8 +161,7 @@ type RecentTurn = { bytes: number } -/** Bounded terminal lifecycle window: exact echoes that arrive after completion - * revise it, and a later end for a turn in it is not a second settlement. */ +/** Bounded terminal lifecycle window: exact echoes that arrive after completion revise it. */ export class CodexJournalRecentTurns { private readonly turns = new Map() private retainedBytes = 0 @@ -232,10 +225,6 @@ export class CodexJournalRecentTurns { } } - has(threadId: string, turnId: string): boolean { - return this.turns.has(this.turnKey(threadId, turnId)) - } - requestOriginRevision( threadId: string, turnId: string, diff --git a/src/main/codex/codex-structured-journal-translation-turns.ts b/src/main/codex/codex-structured-journal-translation-turns.ts index 8678c175599..52dca66a35b 100644 --- a/src/main/codex/codex-structured-journal-translation-turns.ts +++ b/src/main/codex/codex-structured-journal-translation-turns.ts @@ -43,12 +43,12 @@ export function codexTurnLifecycleBody( return agentJournalTurnBody(turnLifecycle) } -/** Maps a `turn/completed` status; a missing one is a clean finish. A terminal - * `error` also ends a turn and names its own outcome rather than coming here. */ +/** Maps a `turn/completed` status, live or restored. Only `interrupted` is a stop; + * a failed turn completed, and `codexTurnOutcome` says it failed. */ export function codexTurnLifecycleState( status: string | null ): Extract { - return status === null || status === 'completed' ? 'completed' : 'interrupted' + return status === 'interrupted' ? 'interrupted' : 'completed' } /** diff --git a/src/main/codex/codex-structured-journal-translation-writers.ts b/src/main/codex/codex-structured-journal-translation-writers.ts index c4fba2c1370..72c1026452d 100644 --- a/src/main/codex/codex-structured-journal-translation-writers.ts +++ b/src/main/codex/codex-structured-journal-translation-writers.ts @@ -8,7 +8,6 @@ import { CodexJournalGenericFrames } from './codex-structured-journal-generic-fr import { CodexJournalGoals } from './codex-structured-journal-goals' import { CodexJournalItems } from './codex-structured-journal-items' import { CodexJournalPrompts } from './codex-structured-journal-prompts' -import { createCodexOversizedNotificationSettler } from './codex-structured-journal-translation-frames' import { CodexJournalActiveTurns } from './codex-structured-journal-translation-turn-state' import { CodexSubagentRoster } from './codex-subagent-roster' @@ -39,7 +38,6 @@ export function createCodexJournalTranslatorWriters(deps: CodexJournalTranslator producerDeps, (threadId, itemId) => items.detailFor(threadId, itemId), activeTurn - ), - settleOversizedNotification: createCodexOversizedNotificationSettler(producerDeps, items) + ) } } diff --git a/src/main/codex/codex-structured-journal-translation.test.ts b/src/main/codex/codex-structured-journal-translation.test.ts index d902b7b4725..676d9054ed7 100644 --- a/src/main/codex/codex-structured-journal-translation.test.ts +++ b/src/main/codex/codex-structured-journal-translation.test.ts @@ -287,7 +287,7 @@ describe('codex journal translation', () => { ).toBe('idle') }) - describe('a terminal error ends the turn it names', () => { + describe('a turn-ending error is a row on the turn it names, and the completion ends it', () => { function statusOf(rows: readonly Row[]) { return projectStructuredAgentSessionStatus( reduced(rows).map((row, sequence) => ({ @@ -308,7 +308,11 @@ describe('codex journal translation', () => { }) } - it('settles the turn and keeps the provider sentence as its own row', () => { + const FAILED_COMPLETION = notification('turn/completed', { + turn: { id: TURN_ID, status: 'failed', durationMs: 2_400 } + }) + + it('keeps the turn working through the error, and the failed completion settles it', () => { const tap = recorder() const translator = createCodexJournalTranslator({ sink: tap.sink, @@ -316,17 +320,21 @@ describe('codex journal translation', () => { }) translator.handle(TURN_STARTED) - expect(statusOf(tap.rows)).toBe('working') - translator.handle(errorNotification({ turnId: TURN_ID, willRetry: false })) + expect(statusOf(tap.rows)).toBe('working') + expect(tap.rows.filter((row) => row.body.kind === 'turn')).toHaveLength(1) + + translator.handle(FAILED_COMPLETION) + expect(statusOf(tap.rows)).toBe('idle') expect(reduced(tap.rows).map((row) => row.body)).toContainEqual( expect.objectContaining({ kind: 'turn', turnId: TURN_ID, state: 'completed', - outcome: 'failure' + outcome: 'failure', + durationMs: 2_400 }) ) // The message the user reads is still a row of its own. @@ -353,7 +361,7 @@ describe('codex journal translation', () => { expect(tap.rows.filter((row) => row.body.kind === 'turn')).toHaveLength(1) }) - it('does not overwrite the terminal row of a turn that already completed', () => { + it('writes only its row for an error after the turn completed', () => { const tap = recorder() const translator = createCodexJournalTranslator({ sink: tap.sink, @@ -361,21 +369,16 @@ describe('codex journal translation', () => { }) translator.handle(TURN_STARTED) - translator.handle( - notification('turn/completed', { - turn: { id: TURN_ID, status: 'failed', durationMs: 4_000 } - }) - ) + translator.handle(FAILED_COMPLETION) const settled = reduced(tap.rows).find((row) => row.body.kind === 'turn')?.body translator.handle(errorNotification({ turnId: TURN_ID, willRetry: false })) - // The completion carries the duration a late error could not reconstruct. expect(reduced(tap.rows).find((row) => row.body.kind === 'turn')?.body).toEqual(settled) expect(statusOf(tap.rows)).toBe('idle') }) - it('settles the running turn when the error names no turn', () => { + it('settles nothing when the error names no turn', () => { const tap = recorder() const translator = createCodexJournalTranslator({ sink: tap.sink, @@ -385,21 +388,8 @@ describe('codex journal translation', () => { translator.handle(TURN_STARTED) translator.handle(errorNotification({ willRetry: false })) - expect(statusOf(tap.rows)).toBe('idle') - }) - - it('does not reopen a running row when the completion arrives after the error', () => { - const tap = recorder() - const translator = createCodexJournalTranslator({ - sink: tap.sink, - primaryThreadId: () => THREAD_ID - }) - - translator.handle(TURN_STARTED) - translator.handle(errorNotification({ turnId: TURN_ID, willRetry: false })) - translator.handle(notification('turn/completed', { turn: { id: TURN_ID, status: 'failed' } })) - - expect(statusOf(tap.rows)).toBe('idle') + expect(statusOf(tap.rows)).toBe('working') + expect(tap.rows.filter((row) => row.body.kind === 'turn')).toHaveLength(1) }) }) @@ -446,14 +436,12 @@ describe('codex journal translation', () => { expect(stopped).toEqual([]) }) - it('releases after systemError arrives before the terminal error notification', () => { + it('releases after the failed completion that follows systemError and the error', () => { const stopped: string[] = [] const { translator } = translatorReporting(stopped) translator.handle(TURN_STARTED) translator.handle(statusChanged('systemError')) - expect(stopped).toEqual([]) - translator.handle( notification('error', { turnId: TURN_ID, @@ -461,6 +449,9 @@ describe('codex journal translation', () => { error: { message: 'fatal' } }) ) + expect(stopped).toEqual([]) + + translator.handle(notification('turn/completed', { turn: { id: TURN_ID, status: 'failed' } })) expect(stopped).toEqual([THREAD_ID]) }) diff --git a/src/main/codex/codex-structured-journal-translation.ts b/src/main/codex/codex-structured-journal-translation.ts index fb90cb73128..fadab8b4589 100644 --- a/src/main/codex/codex-structured-journal-translation.ts +++ b/src/main/codex/codex-structured-journal-translation.ts @@ -11,9 +11,8 @@ import { restoreCodexJournalThread } from './codex-structured-journal-translatio import { CodexJournalTurnBoundaries } from './codex-structured-journal-translation-turn-boundaries' import { createCodexJournalTranslatorWriters } from './codex-structured-journal-translation-writers' import { publishCodexTurnLifecycle } from './codex-structured-journal-translation-turns' -import { readCodexProviderVerdict } from './codex-structured-journal-provider-verdicts' import { createCodexThreadItemRouter } from './codex-structured-journal-thread-item-routing' -import { readCodexTurnId } from './codex-structured-thread-facts' +import { codexThreadStoppedRunning, readCodexTurnId } from './codex-structured-thread-facts' import type { CodexStructuredSessionEvent } from './codex-structured-session-adapter' export type { @@ -36,17 +35,8 @@ export { export function createCodexJournalTranslator( deps: CodexJournalTranslatorDeps ): CodexJournalTranslator { - const { - activeTurns, - subagents, - linkageFor, - genericFrames, - items, - compactions, - goals, - prompts, - settleOversizedNotification - } = createCodexJournalTranslatorWriters(deps) + const { activeTurns, subagents, linkageFor, genericFrames, items, compactions, goals, prompts } = + createCodexJournalTranslatorWriters(deps) const flushStreams = (): CodexJournalTranslationAdmission => items.streams.flush() ? CODEX_JOURNAL_ADMITTED : { accepted: false, reason: 'backpressure' } let readActivity = createCodexProviderActivityReader() @@ -205,10 +195,6 @@ export function createCodexJournalTranslator( ) } if (event.type === 'provider-frame') { - const settlement = settleOversizedNotification(event) - if (settlement && !settlement.accepted) { - return settlement - } return genericFrames.appendUnhandled(event.kind, event.payload, event.threadId) } if (event.method === 'turn/started' || event.method === 'turn/completed') { @@ -249,28 +235,24 @@ export function createCodexJournalTranslator( return publishActivity(event, routed) } } - const verdict = readCodexProviderVerdict(event.method, event.params) + // A thread that stopped running settles no open turn: Codex clears `running` + // on every error, and an open turn ends on its `turn/completed`. It releases + // a send whose dispatch was never answered, which nothing else re-derives live. if ( - verdict === 'thread-stopped-running' && + event.method === 'thread/status/changed' && + codexThreadStoppedRunning(event.params) && event.threadId === (deps.primaryThreadId?.() ?? null) ) { primaryThreadStoppedRunning = true reportPrimaryThreadStoppedRunning() } - // The row carries the provider's sentence and is written first, so it lands - // inside the turn this same frame is about to end. + // A turn-ending `error` is a row inside the turn it names; the failed + // `turn/completed` Codex sends after it is that turn's end. const unhandled = genericFrames.appendUnhandled( `notification:${event.method}`, event.params, event.threadId ) - if (unhandled.accepted && verdict === 'turn-failed') { - const failed = turnBoundaries.fail(event) - if (!failed.accepted) { - return failed - } - reportPrimaryThreadStoppedRunning() - } return publishActivity(event, unhandled) }, cancelPrompt: (journalItemId) => prompts.cancel(journalItemId), diff --git a/src/main/codex/codex-structured-journal-turn-endings-replay.test.ts b/src/main/codex/codex-structured-journal-turn-endings-replay.test.ts new file mode 100644 index 00000000000..d1de7e74766 --- /dev/null +++ b/src/main/codex/codex-structured-journal-turn-endings-replay.test.ts @@ -0,0 +1,206 @@ +// Real `codex app-server` frame orders, replayed through the translator. +// +// The fixture is trimmed from captures of the real binary on 0.141.0 (the oldest +// version Orca exercises) and 0.158.0, with only the upstream provider faked: +// statuses, `willRetry`, the provider's sentence and `durationMs`, at the host +// receipt time `t` each frame arrived. Ids are replaced with synthetic ones. + +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import type { + AgentJournalItemBody, + AgentJournalItemIdentity +} from '../../shared/agent-session-journal-types' +import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { createCodexJournalTranslator } from './codex-structured-journal-translation' +import { + readCodexThreadId, + readCodexTurnDurationMs, + readCodexTurnId, + readCodexTurnStatus +} from './codex-structured-thread-facts' + +const SESSION_ID = 'session-1' + +type CapturedFrame = { case: string; t: number; method: string; params: Record } +type Row = { key: string; body: AgentJournalItemBody } + +const FRAMES: CapturedFrame[] = readFileSync( + join(__dirname, '__fixtures__', 'codex-app-server-turn-endings.jsonl'), + 'utf8' +) + .split('\n') + .filter((line) => line.length > 0) + .map((line) => JSON.parse(line)) + +function framesOf(name: string): CapturedFrame[] { + const frames = FRAMES.filter((frame) => frame.case === name) + expect(frames.length).toBeGreaterThan(0) + return frames +} + +/** Replays a case's frames, or only its first `count`. */ +function replay(name: string, count = Number.POSITIVE_INFINITY) { + const frames = framesOf(name).slice(0, count) + const threadId = readCodexThreadId(frames[0]?.params) ?? '' + const rows: Row[] = [] + const sink: StructuredAgentSessionEventSink = { + appendItem: (identity: AgentJournalItemIdentity, body) => + rows.push({ key: agentJournalItemKey(identity), body }), + appendTombstone: () => {}, + publish: () => {} + } + const translator = createCodexJournalTranslator({ + sink, + sessionId: SESSION_ID, + primaryThreadId: () => threadId + }) + for (const frame of frames) { + expect( + translator.handle({ + type: 'notification', + sessionId: SESSION_ID, + threadId, + method: frame.method, + params: frame.params, + observedAt: frame.t + }) + ).toEqual({ accepted: true }) + } + return { frames, rows, translator } +} + +function turnWrites(rows: readonly Row[], turnId: string) { + return rows + .map((row) => row.body) + .filter((body) => body.kind === 'turn' && body.turnId === turnId) +} + +function terminalWrites(rows: readonly Row[], turnId: string) { + return turnWrites(rows, turnId).filter((body) => body.kind === 'turn' && body.state !== 'running') +} + +function receipt(frames: readonly CapturedFrame[], method: string, turnId: string): number { + const frame = frames.find( + (entry) => entry.method === method && readCodexTurnId(entry.params) === turnId + ) + if (!frame) { + throw new Error(`no ${method} for ${turnId}`) + } + return frame.t +} + +function completion(frames: readonly CapturedFrame[], turnId: string) { + const frame = frames.find( + (entry) => entry.method === 'turn/completed' && readCodexTurnId(entry.params) === turnId + ) + return frame + ? { + t: frame.t, + status: readCodexTurnStatus(frame.params), + durationMs: readCodexTurnDurationMs(frame.params) + } + : null +} + +const VERDICT: Record = { + completed: { state: 'completed', outcome: 'success' }, + failed: { state: 'completed', outcome: 'failure' }, + interrupted: { state: 'interrupted', outcome: 'cancellation' } +} + +const ENDED_CASES = [...new Set(FRAMES.map((frame) => frame.case))].filter( + (name) => name !== '0.158.0-conn-refused' +) + +describe('captured Codex turns end once, on their own completion', () => { + it.each(ENDED_CASES)('%s', (name) => { + const { frames, rows } = replay(name) + const turnIds = frames + .filter((frame) => frame.method === 'turn/started') + .map((frame) => readCodexTurnId(frame.params) ?? '') + + expect(turnIds.length).toBeGreaterThan(0) + for (const turnId of turnIds) { + const end = completion(frames, turnId) + expect(end).not.toBeNull() + if (!end) { + continue + } + const verdict = VERDICT[end.status ?? ''] + expect(verdict).toBeDefined() + // One terminal write per turn, and it is Codex's own completion: its time, + // its duration and its verdict, with the start this host saw. + expect(terminalWrites(rows, turnId)).toEqual([ + expect.objectContaining({ + state: verdict?.state, + outcome: verdict?.outcome, + startedAt: receipt(frames, 'turn/started', turnId), + completedAt: end.t, + durationMs: end.durationMs + }) + ]) + } + }) + + it('keeps every failed turn working through its error until the completion', () => { + for (const name of ENDED_CASES) { + const frames = framesOf(name) + const failedTurns = frames + .filter((frame) => frame.method === 'error' && frame.params.willRetry === false) + .map((frame) => readCodexTurnId(frame.params) ?? '') + for (const turnId of failedTurns) { + const errorAt = frames.findIndex( + (frame) => + frame.method === 'error' && + frame.params.willRetry === false && + readCodexTurnId(frame.params) === turnId + ) + // Through the error, the turn is open; the completion right after it ends it. + expect(terminalWrites(replay(name, errorAt + 1).rows, turnId)).toEqual([]) + expect(terminalWrites(replay(name).rows, turnId)).toHaveLength(1) + } + } + }) + + it('keeps a turn Codex is still retrying on 0.158.0 running, and the exit sweep ends it', () => { + const { frames, rows, translator } = replay('0.158.0-conn-refused') + const turnId = + readCodexTurnId(frames.find((frame) => frame.method === 'turn/started')?.params) ?? '' + const retries = frames.filter((frame) => frame.method === 'error') + + // Seven "Reconnecting... waiting for network" frames over four minutes, all + // willRetry, and no completion: the turn is genuinely still open. + expect(retries).toHaveLength(7) + expect(retries.every((frame) => frame.params.willRetry === true)).toBe(true) + expect(turnWrites(rows, turnId)).toEqual([ + expect.objectContaining({ + state: 'running', + startedAt: receipt(frames, 'turn/started', turnId) + }) + ]) + + translator.handle({ + type: 'ended', + sessionId: SESSION_ID, + reason: 'lost child', + cause: 'unexpected-exit', + fence: 1, + acquisitionGeneration: 'generation-1', + observedAt: 250_000 + }) + + expect(terminalWrites(rows, turnId)).toEqual([ + { + kind: 'turn', + turnId, + state: 'interrupted', + userItemId: `codex:${readCodexThreadId(frames[0]?.params)}:${turnId}:0`, + startedAt: receipt(frames, 'turn/started', turnId), + completedAt: 250_000 + } + ]) + }) +}) diff --git a/src/main/codex/codex-structured-journal-turn-settles-once-journal.test.ts b/src/main/codex/codex-structured-journal-turn-settles-once-journal.test.ts index 6459b59755c..4e3caaa8bc0 100644 --- a/src/main/codex/codex-structured-journal-turn-settles-once-journal.test.ts +++ b/src/main/codex/codex-structured-journal-turn-settles-once-journal.test.ts @@ -73,8 +73,14 @@ async function session() { } } +function turnRecord(rows: Awaited>['items']>>) { + return rows + .map((item) => readAgentJournalTurn(item.body)) + .findLast((record) => record?.turnId === TURN) +} + describe('a failed Codex turn in the journal', () => { - it('keeps the failure and its duration when the failed completion lands while the error is still queued', async () => { + it('keeps the failed completion, its duration and the start, when it lands while the error is still queued', async () => { const { on, drained, items } = await session() on('turn/started', { turn: { id: TURN } }, 1_000) on( @@ -85,7 +91,7 @@ describe('a failed Codex turn in the journal', () => { await drained() // Codex writes both frames back to back; the error's status row is still being - // written when the failed completion arrives, so the error's settlement is queued. + // written when the failed completion arrives. on( 'error', { turnId: TURN, willRetry: false, error: { message: 'stream disconnected' } }, @@ -94,18 +100,50 @@ describe('a failed Codex turn in the journal', () => { on('turn/completed', { turn: { id: TURN, status: 'failed', durationMs: 1_900 } }, 3_100) const rows = await items() - const turn = rows - .map((item) => readAgentJournalTurn(item.body)) - .findLast((record) => record?.turnId === TURN) - expect(turn).toMatchObject({ + expect(turnRecord(rows)).toMatchObject({ state: 'completed', outcome: 'failure', startedAt: 1_000, - completedAt: 3_000 + completedAt: 3_100, + durationMs: 1_900 }) - // The duration "Worked for" shows under the turn's message. + // "Worked for" under the turn's message reads Codex's own duration. expect( completedStructuredAgentTurnSeconds(selectStructuredAgentRunningTurnTiming(rows, TURN)) - ).toBe(2) + ).toBe(1) }) + + // Codex ends a turn once, so a second completion is not expected. If one came, + // the settlement id keeps the first record, whether it is still queued or written. + it.each([ + ['still queued', false], + ['already written', true] + ] as const)( + 'keeps the first completion when a second one arrives while the first is %s', + async (_label, drainFirst) => { + const { on, drained, items } = await session() + on('turn/started', { turn: { id: TURN } }, 1_000) + await drained() + + // The reply's row is being written, so the first completion waits behind it. + on( + 'item/completed', + { turnId: TURN, item: { type: 'agentMessage', id: 'agent-1', text: 'Done' } }, + 1_900 + ) + on('turn/completed', { turn: { id: TURN, status: 'completed', durationMs: 900 } }, 2_000) + if (drainFirst) { + await drained() + } + on('turn/completed', { turn: { id: TURN, status: 'failed' } }, 3_000) + + expect(turnRecord(await items())).toMatchObject({ + state: 'completed', + outcome: 'success', + startedAt: 1_000, + completedAt: 2_000, + durationMs: 900 + }) + } + ) }) diff --git a/src/main/codex/codex-structured-thread-facts.ts b/src/main/codex/codex-structured-thread-facts.ts index 755006d17fa..aaf65f93ec6 100644 --- a/src/main/codex/codex-structured-thread-facts.ts +++ b/src/main/codex/codex-structured-thread-facts.ts @@ -62,15 +62,17 @@ export function readCodexTurnDurationMs(payload: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null } -/** `error` carries `willRetry`: Codex sets it on a stream error it is about to - * retry, and omits it (false) on one that ended the turn the frame names. */ +/** `thread/status/changed` carries a TAGGED status (`{status:{type}}`), never a + * bare string. `idle` and `systemError` are the two arms that mean the thread + * is not running; `active` and `notLoaded` are not. */ +/** + * Codex hardcodes `willRetry: false` on an `error` that affects turn status, and `true` on a + * stream error it is about to retry, which ends nothing. + */ export function readCodexErrorWillRetry(payload: unknown): boolean { return record(payload)?.willRetry === true } -/** `thread/status/changed` carries a TAGGED status (`{status:{type}}`), never a - * bare string. `idle` and `systemError` are the two arms that mean the thread - * is not running; `active` and `notLoaded` are not. */ export function codexThreadStoppedRunning(payload: unknown): boolean { const type = record(record(payload)?.status)?.type return type === 'idle' || type === 'systemError' diff --git a/src/main/daemon/serialize-grid-transcript-replay.test.ts b/src/main/daemon/serialize-grid-transcript-replay.test.ts index dca03b69a05..298aaa681bf 100644 --- a/src/main/daemon/serialize-grid-transcript-replay.test.ts +++ b/src/main/daemon/serialize-grid-transcript-replay.test.ts @@ -43,6 +43,11 @@ const KNOWN_PREEXISTING_I2_FAILURES: Record = { 'codex-0157-effort-override-embedded-warning': 4, 'codex-0157-no-daemon-effort-override': 16, 'codex-0157-plain-ready': 18, + // Same extra dim bit on the 0.157/0.158 header row (STA-8834 fixtures). + 'codex-0-157-1-update-dialog': 16, + 'codex-0-158-0-approval': 12, + 'codex-0-158-0-timed-turn': 20, + 'codex-0-158-0-trustprompt': 36, 'claude-dialog-trust-workspace-answered': 13, // DSH-TUI's whale intro paints whole rows of 24-bit background, and every one of this // transcript's divergences is the same shape: `visible-grid row=0`, a true-colour diff --git a/src/main/git/repo-api-parity.test.ts b/src/main/git/repo-api-parity.test.ts deleted file mode 100644 index 9143b7bf65f..00000000000 --- a/src/main/git/repo-api-parity.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest' -import * as repo from './repo' - -describe('repo public API parity', () => { - it('keeps the historical runtime export surface', () => { - expect(Object.keys(repo).sort()).toEqual( - [ - 'DEFAULT_BASE_REF_PROBES', - 'buildSearchBaseRefsArgv', - 'getBaseRefDefault', - 'getBranchConflictKind', - 'getDefaultBaseRef', - 'getDefaultRemote', - 'getGitRepoRoot', - 'getLinkedWorktreeMainRepoRoot', - 'getRecentDriftSubjects', - 'getRemoteCommitUrl', - 'getRemoteCount', - 'getRemoteDrift', - 'getRemoteFileUrl', - 'getRemoteUrl', - 'getRepoName', - 'isForEachRefExcludeUnsupportedError', - 'isGitRepo', - 'mergeBaseRefSearchResultGroups', - 'normalizeGitRepoRootForInputPath', - 'normalizeRefSearchQuery', - 'parseAndFilterSearchRefDetails', - 'parseRemoteCount', - 'resolveDefaultBaseRefViaExec', - 'resolveDefaultBaseRefWithLocalGit', - 'searchBaseRefDetails', - 'searchBaseRefs' - ].sort() - ) - }) -}) diff --git a/src/main/ipc/pty/ipc/spawn-commit-persist.ts b/src/main/ipc/pty/ipc/spawn-commit-persist.ts index 3740e1489a0..46fd8e714bb 100644 --- a/src/main/ipc/pty/ipc/spawn-commit-persist.ts +++ b/src/main/ipc/pty/ipc/spawn-commit-persist.ts @@ -32,7 +32,7 @@ export async function persistPtyIpcSpawnCommit(ctx: PtyIpcSpawnState): Promise

+ /** Fence of the writer that created each item: the generation a running turn belongs to. */ + itemFences: Map /** Revision of a removed item, so a late lower revision cannot resurrect it. */ tombstones: Map submissions: Map @@ -61,6 +63,7 @@ export function createJournalReducerState(sessionId: string, epoch: string): Jou oldestSequence: 1, highestFence: 0, items: new Map(), + itemFences: new Map(), tombstones: new Map(), submissions: new Map(), receipts: new Map(), @@ -82,7 +85,13 @@ export function applyJournalRow(state: JournalReducerState, row: JournalRow): vo } const itemId = resolveJournalItemId(state, row.itemId, row.body) acceptSubmissionFromProviderItem(state, row.itemId, itemId, row) - upsertItem(state, itemId, row.revision, journalRenderItem(itemId, row.revision, row.body, row)) + upsertItem( + state, + itemId, + row.revision, + journalRenderItem(itemId, row.revision, row.body, row), + row.fence + ) return } if (row.kind === 'tombstone') { @@ -103,7 +112,7 @@ export function applyJournalRow(state: JournalReducerState, row: JournalRow): vo acceptSubmissionFromProviderItem(state, mutation.itemId, itemId, row) const producer = journalBatchMutationProducer(row, mutation) const item = journalRenderItem(itemId, revision, body, row, producer, sequenceIndex) - upsertItem(state, itemId, revision, item) + upsertItem(state, itemId, revision, item, row.fence) } else { removeItem(state, resolveItemId(state, mutation.itemId), mutation.revision) } @@ -187,7 +196,8 @@ function upsertItem( state: JournalReducerState, itemId: string, revision: number, - next: AgentJournalRenderItem + next: AgentJournalRenderItem, + fence: number ): void { const tombstoned = state.tombstones.get(itemId) if (tombstoned !== undefined && revision <= tombstoned) { @@ -199,6 +209,7 @@ function upsertItem( } if (!existing) { state.items.set(itemId, next) + state.itemFences.set(itemId, fence) state.tombstones.delete(itemId) return } @@ -237,6 +248,7 @@ function removeItem(state: JournalReducerState, itemId: string, revision: number } state.tombstones.set(itemId, revision) state.items.delete(itemId) + state.itemFences.delete(itemId) } function applySubmission( @@ -255,7 +267,7 @@ function applySubmission( ...(row.handoverRecorded ? { handoverRecorded: true, acceptedSequence: row.seq } : {}) }) const itemId = agentJournalSubmissionKey(row.clientMessageId) - upsertItem(state, itemId, 0, journalRenderItem(itemId, 0, row.body, row)) + upsertItem(state, itemId, 0, journalRenderItem(itemId, 0, row.body, row), row.fence) } function acceptSubmissionFromProviderItem( diff --git a/src/main/native-chat/agent-session-journal/journal-store.ts b/src/main/native-chat/agent-session-journal/journal-store.ts index 45c42b45f8a..3be3aebbc51 100644 --- a/src/main/native-chat/agent-session-journal/journal-store.ts +++ b/src/main/native-chat/agent-session-journal/journal-store.ts @@ -234,6 +234,9 @@ export class AgentSessionJournal { /** Includes revisions and completion tombstones, whose timestamps disappear from render items. */ lastActivityAt = (): number => this.state.lastActivityAt + /** Fence of the writer that created the item, while it is in the timeline. */ + itemFence = (itemId: string): number | undefined => this.state.itemFences.get(itemId) + submissions = (): AgentJournalSubmission[] => [...this.state.submissions.values()] pendingSubmissions = (): AgentJournalSubmission[] => diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.ts index bce79ca71f8..dd8853954e3 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.ts @@ -10,6 +10,7 @@ import type { AgentJournalResetReason } from '../../../shared/agent-session-journal-types' import { journalDirectoryFor } from '../agent-session-journal/journal-paths' +import type { AgentSessionJournal } from '../agent-session-journal/journal-store' import { openAgentSessionJournalWithRecovery } from './agent-session-journal-recovery' import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope' import type { AgentSessionRecord } from '../../../shared/agent-session-record' @@ -103,23 +104,11 @@ export async function openStructuredAgentSessionConversationJournal( } catch (error) { deps.onEventSinkError?.({ sessionId, error }) } - try { - // No child in this process writes to a journal nobody had open, so whatever it shows running - // belongs to a generation that is gone, whatever the lease still claims. Settled before any - // reader or child sees it. - if (!options.acquisition) { - await settleStaleStructuredAgentSessionState({ - journal: opened.journal, - sessionId, - fence, - acquisitionGeneration: null, - deathEvidence: record.lease.deathEvidence ?? null, - failureTextContext: structuredAgentSessionFailureWordsContext(record) - }) - } - } catch (error) { - // Best effort: the next acquire re-derives it. - deps.onEventSinkError?.({ sessionId, error }) + // No child in this process writes to a journal nobody had open, so whatever it shows running + // belongs to a generation that is gone, whatever the lease still claims. Settled before any + // reader or child sees it. + if (!options.acquisition) { + await settleGoneGeneration(deps, record, opened.journal) } return { session: { journal: opened.journal, params, child: null }, @@ -127,6 +116,42 @@ export async function openStructuredAgentSessionConversationJournal( } } +/** + * The open's settle again, for a conversation already open: a proof of death written since it + * opened (the startup reconcile, a recovery) revises what the open could only call `unverifiable`. + * A record holds a proof only while released, so no child here is writing. A no-op once revised. + */ +export async function resettleOpenStructuredAgentSessionConversation( + deps: StructuredAgentSessionConversationOpenDeps, + sessionId: string, + session: StructuredAgentSessionHostSession | undefined +): Promise { + const record = deps.store.getRecord(sessionId) + if (session && record?.lease.deathEvidence) { + await settleGoneGeneration(deps, record, session.journal) + } +} + +async function settleGoneGeneration( + deps: Pick, + record: AgentSessionRecord, + journal: AgentSessionJournal +): Promise { + try { + await settleStaleStructuredAgentSessionState({ + journal, + sessionId: record.sessionId, + fence: record.lease.runtimeFence, + acquisitionGeneration: null, + deathEvidence: record.lease.deathEvidence ?? null, + failureTextContext: structuredAgentSessionFailureWordsContext(record) + }) + } catch (error) { + // Best effort: the next open or acquire re-derives it. + deps.onEventSinkError?.({ sessionId: record.sessionId, error }) + } +} + export function attachParamsForRecord( record: AgentSessionRecord, input: { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts new file mode 100644 index 00000000000..0d83491ee21 --- /dev/null +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts @@ -0,0 +1,528 @@ +// Where a turn a crash cut short ends, when the provider wrote nothing while it worked. +// +// Claude reports a Bash call once when it starts and again only when it finishes, so a command +// that runs for half a minute leaves one journal row at its start. The lease renewal the host +// wrote every ten seconds is what saw the child working after that row. + +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { AgentSessionOwnerProbe } from '../../../shared/agent-session-lease-adjudication' +import type { AgentSessionRecord } from '../../../shared/agent-session-record' +import type { AgentSessionSubscribeEvent } from '../../../shared/agent-session-wire' +import { readAgentJournalTurn } from '../../../shared/agent-session-turn-record' +import { + completedStructuredAgentTurnSeconds, + selectStructuredAgentTurnTimings +} from '../../../shared/structured-agent-session-turn-timing' +import { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' +import { AGENT_SESSION_STORE_FILE_NAME } from '../../runtime/agent-session-record-store-file' +import { journalDirectoryFor } from '../agent-session-journal/journal-paths' +import { openAgentSessionJournal } from '../agent-session-journal/journal-store-factory' +import { + AgentSessionAcquisitionExitUnprovenError, + type StructuredAgentSessionAdapter +} from './structured-agent-session-adapter' +import { resettleOpenStructuredAgentSessionConversation } from './structured-agent-session-conversation-open' +import { StructuredAgentSessionHost } from './structured-agent-session-host' +import { STRUCTURED_AGENT_SESSION_IDLE_MS } from './structured-agent-session-idle-sweep' +import type { StructuredAgentSessionHostDeps } from './structured-agent-session-host-types' +import { + hostTestAttachParams, + HOST_TEST_LOCATION as LOCATION, + HOST_TEST_SESSION as SESSION +} from './structured-agent-session-host-test-data' + +const PROVIDER_SESSION = 'provider-session-alpha-1' +/** The tool call's row: the last thing the provider wrote before the crash. */ +const TOOL_STARTED_AT = 1_800_000_000_000 +/** The last renewal before the crash, while the command was still running. */ +const LAST_RENEWED_AT = TOOL_STARTED_AT + 25_000 +/** Orca comes back an hour later. */ +const RELAUNCHED_AT = TOOL_STARTED_AT + 60 * 60 * 1000 + +let root: string +let store: AgentSessionRecordStore +let host: StructuredAgentSessionHost + +function crashedClaudeRecord(): AgentSessionRecord { + const linkId = 'claude-13-link' + return { + schemaVersion: 2, + sessionId: SESSION, + location: LOCATION, + provider: 'claude', + providerHandleChain: [ + { + linkId, + handle: { provider: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null }, + origin: 'created', + mintedAtFence: 13, + observedAt: TOOL_STARTED_AT - 60_000 + } + ], + accountHome: { variable: 'CLAUDE_CONFIG_DIR', path: '/home/dev/.claude' }, + createdAt: TOOL_STARTED_AT - 60_000, + updatedAt: LAST_RENEWED_AT, + lease: { + sessionId: SESSION, + runtimeKind: 'native', + runtimeFence: 13, + handoffStage: null, + provenHandleLinkId: linkId, + ownerProcess: { + hostId: 'local', + pid: 12_546, + processStartTimeMs: TOOL_STARTED_AT - 60_000, + spawnToken: 'spawn-crashed' + }, + reservedSpawnToken: 'spawn-crashed', + leaseDeadlineAt: LAST_RENEWED_AT + 30_000, + lastRenewedAt: LAST_RENEWED_AT, + handoffOperationId: null, + journalCheckpoint: null, + claimKeyId: 'key-1', + claimStatus: 'live', + unreconciled: false, + deathEvidence: null + } + } +} + +async function seedCrashedStore(): Promise { + const directory = join(root, 'store') + await mkdir(directory, { recursive: true }) + await writeFile( + join(directory, AGENT_SESSION_STORE_FILE_NAME), + JSON.stringify({ + schemaVersion: 2, + hostId: 'local', + records: { [SESSION]: crashedClaudeRecord() }, + operations: {}, + retiredClaimKeys: [], + unusableRecords: {} + }), + 'utf-8' + ) + store = await AgentSessionRecordStore.open({ directory, hostId: 'local' }) +} + +/** A running turn whose only row after its start is a Bash call that never reported back, for a + * send the provider never acknowledged: the reopen settles it at the fence after the crash. */ +async function seedClaudeToolTurn(): Promise { + let now = TOOL_STARTED_AT - 2_000 + const journal = await openAgentSessionJournal({ + identity: { + sessionId: SESSION, + workspaceId: LOCATION.workspaceId, + hostId: LOCATION.executionHostId, + agent: 'claude', + providerHandle: { kind: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null } + }, + journalDir: journalDirectoryFor(root, { + workspaceId: LOCATION.workspaceId, + sessionId: SESSION + }), + now: () => now + }) + await journal.appendSubmission({ + clientMessageId: 'send-1', + payloadFingerprint: '0'.repeat(64), + body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'run the loop' }] }, + fence: 13 + }) + await journal.appendItem( + { provider: 'claude', sessionId: PROVIDER_SESSION, uuid: 'uuid-turn' }, + { kind: 'turn', turnId: 'turn-1', state: 'running', startedAt: now }, + { fence: 13 } + ) + now = TOOL_STARTED_AT + await journal.appendItem( + { provider: 'claude', sessionId: PROVIDER_SESSION, uuid: 'uuid-bash' }, + { + kind: 'tool-call', + name: 'Bash', + input: { command: 'for i in $(seq 90); do sleep 1; done' }, + state: 'running' + }, + { fence: 13 } + ) + await journal.close() +} + +function openHost(overrides: Partial): void { + host = new StructuredAgentSessionHost({ + store, + adapter: { + acquire: vi.fn(), + dispatch: vi.fn(), + cancelTurn: vi.fn(), + answerPrompt: vi.fn(), + setOption: vi.fn(), + supportsCreate: () => true + }, + journalRoot: root, + claimKeyId: 'key-1', + mintSpawnToken: () => 'spawn-new', + now: () => RELAUNCHED_AT, + ...overrides + }) +} + +/** What a client reads over the wire. */ +async function settledTurn() { + return (await host.journalSnapshot(SESSION)).items + .map((item) => readAgentJournalTurn(item.body)) + .find(Boolean) +} + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-crash-turn-end-')) + await seedCrashedStore() + await seedClaudeToolTurn() +}) + +afterEach(async () => { + await host?.flushAllStreamedEvents() + await rm(root, { recursive: true, force: true }) +}) + +describe('a turn a crash cut short mid-tool', () => { + it('ends at the last renewal, not at the tool call the provider last reported', async () => { + openHost({ probeOwner: async () => ({ outcome: 'pid-absent' }) }) + + await host.restoreReadableSessions() + + expect(store.getRecord(SESSION)?.lease.deathEvidence).toMatchObject({ + kind: 'pid-absent', + observedAt: RELAUNCHED_AT, + lastProvenAliveAt: LAST_RENEWED_AT + }) + expect(await settledTurn()).toMatchObject({ + state: 'interrupted', + completedAt: LAST_RENEWED_AT + }) + // "Worked for 27s", where the tool call's row alone reads 2s. + const [timing] = selectStructuredAgentTurnTimings( + (await host.journalSnapshot(SESSION)).items + ).values() + expect(completedStructuredAgentTurnSeconds(timing)).toBe(27) + }) + + it('ends at the pre-crash renewal when the child outlived Orca and recovery stopped it', async () => { + // The orphan is alive at relaunch, but its output went nowhere: none of that is work shown. + let alive = true + const probeOwner = async (): Promise => + alive + ? { outcome: 'identity-matched', matchedOn: ['spawn-token'] } + : { outcome: 'pid-absent' } + const stopOwnerProcess = vi.fn(() => { + alive = false + }) + openHost({ probeOwner, stopOwnerProcess }) + + await host.restoreReadableSessions() + + expect(stopOwnerProcess).toHaveBeenCalledOnce() + expect(store.getRecord(SESSION)?.lease.deathEvidence).toMatchObject({ + kind: 'pid-absent', + lastProvenAliveAt: LAST_RENEWED_AT + }) + expect(await settledTurn()).toMatchObject({ + state: 'interrupted', + completedAt: LAST_RENEWED_AT + }) + }) +}) + +/** Every turn state a subscriber was sent, in order: snapshots, then live batches. */ +function turnStatesSent(events: readonly AgentSessionSubscribeEvent[]) { + return events + .flatMap((event) => + event.type === 'snapshot' ? event.page.items : event.type === 'batch' ? event.batch.items : [] + ) + .flatMap((item) => readAgentJournalTurn(item.body) ?? []) +} + +function attach(fence: number) { + return host.attach( + { callerKey: 'client-1' }, + hostTestAttachParams(fence, { + provider: 'claude', + agent: 'claude', + accountHome: { variable: 'CLAUDE_CONFIG_DIR', path: '/home/dev/.claude' }, + providerHandle: { kind: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null } + }) + ) +} + +/** Runs whatever this session's serialize already has queued. */ +function drainSession(): Promise { + return host.collaboratorsForTests().serialize(SESSION, async () => {}) +} + +// On desktop the chat on screen at relaunch opens before the startup reconcile has probed its owner, +// so the open can only call the turn unverifiable; the proof, whoever writes it, then revises it. +describe('a turn a read reached before the reconcile proved its owner dead', () => { + it('reads unverifiable, then interrupted at the last renewal, and a subscriber is sent both', async () => { + openHost({ probeOwner: async () => ({ outcome: 'pid-absent' }) }) + const events: AgentSessionSubscribeEvent[] = [] + const unsubscribe = await host.subscribe({ + id: 'reader-1', + sessionId: SESSION, + emit: (event) => events.push(event) + }) + expect(await settledTurn()).toEqual(UNVERIFIABLE_TURN) + + await host.reconcileRestartLeases() + await drainSession() + + // A paired or phone client learns of the revision through the ordinary journal publish. + await vi.waitFor(() => + expect(turnStatesSent(events)).toMatchObject([ + { state: 'unverifiable' }, + { state: 'interrupted', completedAt: LAST_RENEWED_AT } + ]) + ) + const { items } = await host.journalSnapshot(SESSION) + const [timing] = selectStructuredAgentTurnTimings(items).values() + expect(completedStructuredAgentTurnSeconds(timing)).toBe(27) + expect(items.filter((item) => item.body.kind === 'status')).toHaveLength(1) + unsubscribe() + }) + + it('revises nothing twice, whoever re-runs the settle', async () => { + openHost({ probeOwner: async () => ({ outcome: 'pid-absent' }) }) + await host.history({ sessionId: SESSION, direction: 'tail' }) + await host.reconcileRestartLeases() + await drainSession() + const settled = await host.journalSnapshot(SESSION) + + await host + .collaboratorsForTests() + .serialize(SESSION, () => + resettleOpenStructuredAgentSessionConversation( + host.deps, + SESSION, + host.collaboratorsForTests().sessions.get(SESSION) + ) + ) + await host.restoreReadableSessions() + + expect(await host.journalSnapshot(SESSION)).toEqual(settled) + }) + + it('revises it when recovery stops a child that outlived Orca, with no send', async () => { + let alive = true + const probeOwner = async (): Promise => + alive + ? { outcome: 'identity-matched', matchedOn: ['spawn-token'] } + : { outcome: 'pid-absent' } + const stopOwnerProcess = vi.fn(() => { + alive = false + }) + const acquire = vi.fn() + openHost({ + adapter: { + acquire, + dispatch: vi.fn(), + cancelTurn: vi.fn(), + answerPrompt: vi.fn(), + setOption: vi.fn(), + supportsCreate: () => true + }, + probeOwner, + stopOwnerProcess + }) + await host.history({ sessionId: SESSION, direction: 'tail' }) + expect(await settledTurn()).toEqual(UNVERIFIABLE_TURN) + + // The reconcile only parks the live orphan in recovery; recovery's stop is what proves it gone. + await host.restoreReadableSessions() + await drainSession() + + expect(stopOwnerProcess).toHaveBeenCalledOnce() + expect(acquire).not.toHaveBeenCalled() + expect(await settledTurn()).toMatchObject({ + state: 'interrupted', + completedAt: LAST_RENEWED_AT + }) + }) + + it('never touches the turn a start after the crash is writing', async () => { + const acquire = vi.fn( + async ({ fence, spawnToken, onSpawned, events }) => { + const process = { + hostId: 'local', + pid: 8_000, + processStartTimeMs: RELAUNCHED_AT, + spawnToken + } + await onSpawned?.(process) + // The new child is already working when its start lands, ahead of the queued revision. + events?.appendItem( + { provider: 'claude', sessionId: PROVIDER_SESSION, uuid: 'uuid-turn-2' }, + { kind: 'turn', turnId: 'turn-2', state: 'running', startedAt: RELAUNCHED_AT } + ) + return { + process, + link: { + linkId: `claude-${fence}-link`, + handle: { provider: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null }, + origin: 'resumed', + mintedAtFence: fence, + observedAt: RELAUNCHED_AT + } + } + } + ) + openHost({ + adapter: { + acquire, + dispatch: vi.fn(), + cancelTurn: vi.fn(), + answerPrompt: vi.fn(), + setOption: vi.fn(), + supportsCreate: () => true + }, + probeOwner: async () => ({ outcome: 'pid-absent' }) + }) + await host.history({ sessionId: SESSION, direction: 'tail' }) + + // The start runs the reconcile itself, so the revision it queues waits behind the start. + await expect(attach(14)).resolves.toMatchObject({ ok: true }) + await host.flushStreamedEvents(SESSION) + await drainSession() + + const turns = (await host.journalSnapshot(SESSION)).items.flatMap( + (item) => readAgentJournalTurn(item.body) ?? [] + ) + expect(turns).toMatchObject([ + { turnId: 'turn-1', state: 'interrupted', completedAt: LAST_RENEWED_AT }, + { turnId: 'turn-2', state: 'running' } + ]) + }) + + it('stays unverifiable when the revision cannot be written, and a later open revises it', async () => { + let now = RELAUNCHED_AT + const onEventSinkError = vi.fn() + openHost({ + probeOwner: async () => ({ outcome: 'pid-absent' }), + now: () => now, + onEventSinkError + }) + await host.history({ sessionId: SESSION, direction: 'tail' }) + const { journal } = host.collaboratorsForTests().sessions.get(SESSION)! + vi.spyOn(journal, 'appendLifecycleBatch').mockRejectedValueOnce(new Error('disk full')) + + await host.reconcileRestartLeases() + await drainSession() + + expect(onEventSinkError).toHaveBeenCalledOnce() + expect(await settledTurn()).toEqual(UNVERIFIABLE_TURN) + // The proof is durable on the record, so the next open converges. + now += STRUCTURED_AGENT_SESSION_IDLE_MS + 1 + await host.collaboratorsForTests().lifetime.idleSweep.tick() + expect(host.hasSession(SESSION)).toBe(false) + expect(await settledTurn()).toMatchObject({ + state: 'interrupted', + completedAt: LAST_RENEWED_AT + }) + }) +}) + +/** A relaunched host whose first start after the crash, at fence 15, fails with `failure`; every + * later start succeeds. The crashed owner, and any child left behind, probe gone. */ +async function hostWithFailingFirstStart(failure: Error) { + let now = RELAUNCHED_AT + const acquire = vi.fn( + async ({ fence, spawnToken, onSpawned }) => { + const process = { hostId: 'local', pid: 7_000 + fence, processStartTimeMs: now, spawnToken } + await onSpawned?.(process) + if (fence === 15) { + throw failure + } + return { + process, + link: { + linkId: `claude-${fence}-link`, + handle: { provider: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null }, + origin: 'resumed', + mintedAtFence: fence, + observedAt: now + } + } + } + ) + openHost({ + adapter: { + acquire, + releaseAcquisition: async () => true, + dispatch: vi.fn(), + cancelTurn: vi.fn(), + answerPrompt: vi.fn(), + setOption: vi.fn(), + supportsCreate: () => true + }, + probeOwner: async () => ({ outcome: 'pid-absent' }), + now: () => now + }) + await host.reconcileRestartLeases() + return { + attach, + advance: (ms: number) => { + now += ms + } + } +} + +/** The turn as the crashed owner left it, with no end: nothing proves when that owner stopped. */ +const UNVERIFIABLE_TURN = { + turnId: 'turn-1', + state: 'unverifiable', + startedAt: TOOL_STARTED_AT - 2_000 +} + +// The relaunch proved the fence-13 owner gone, but a start reserving fence 15 clears that proof +// before the turn is settled; what the record holds afterwards is about the start's own child. +describe('a turn a newer start could not settle before it failed', () => { + it('is not judged by the death of the child it left for recovery', async () => { + const { attach, advance } = await hostWithFailingFirstStart( + new AgentSessionAcquisitionExitUnprovenError(new Error('hung')) + ) + + await attach(14).catch(() => undefined) + expect(store.getRecord(SESSION)?.lease).toMatchObject({ + runtimeFence: 15, + handoffStage: 'recovering', + lastRenewedAt: RELAUNCHED_AT + }) + advance(60_000) + // Recovery records that child's death, then the client retries at the fence it was told. + await expect(attach(15)).resolves.toMatchObject({ refusal: { currentFence: 16 } }) + expect(store.getRecord(SESSION)?.lease.deathEvidence).toMatchObject({ + ownerFence: 15, + lastProvenAliveAt: RELAUNCHED_AT + }) + await expect(attach(16)).resolves.toMatchObject({ ok: true }) + + expect(await settledTurn()).toEqual(UNVERIFIABLE_TURN) + }) + + it('is not judged by the watched exit of a start that failed', async () => { + const { attach, advance } = await hostWithFailingFirstStart(new Error('claude exited (code 1)')) + + await expect(attach(14)).resolves.toMatchObject({ ok: false }) + expect(store.getRecord(SESSION)?.lease.deathEvidence).toMatchObject({ + kind: 'exit-observed', + ownerFence: 15, + observedAt: RELAUNCHED_AT + }) + advance(60_000) + await expect(attach(16)).resolves.toMatchObject({ ok: true }) + + // Main ended it at the failed start, an hour after the crash, with the start's reason. + expect(await settledTurn()).toEqual(UNVERIFIABLE_TURN) + }) +}) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.ts index f6a166c7c02..8ee3430c5c9 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.ts @@ -25,6 +25,7 @@ import { } from './structured-agent-session-start-failure-row' import type { AgentSessionDeathEvidence } from '../../../shared/agent-session-record' import { + provenUnverifiableTurnRevisions, runningTurnLifecycleRevisions, turnVerdictFromDeathEvidence, type StructuredAgentSessionTurnVerdict @@ -185,9 +186,9 @@ export async function settleStructuredAgentSessionDeadGeneration(input: { /** * Settles whatever a generation with no child in this process left running: found when a new child * is acquired, or when a chat is reopened for reading. Derived from the journal and the lease's - * death evidence each time, so nothing is owed in between. Only an observed exit earns an end time - * and the exit copy. Must run before a new child's buffered events land, or a live turn would be - * judged. + * death evidence each time, so nothing is owed in between. Proven death ends the turn interrupted, + * and a proof written after an earlier settle revises what that settle left `unverifiable`. Must + * run before a new child's buffered events land, or a live turn would be judged. */ export async function settleStaleStructuredAgentSessionState(input: { journal: AgentSessionJournal @@ -200,7 +201,10 @@ export async function settleStaleStructuredAgentSessionState(input: { }): Promise { const { journal } = input const items = journal.snapshot().items - const verdict = turnVerdictFromDeathEvidence(input.deathEvidence) + // Each turn is judged by the evidence only if it names that turn's owner. + const verdictFor = (item: AgentJournalRenderItem) => + turnVerdictFromDeathEvidence(input.deathEvidence, journal.itemFence(item.itemId)) + // Per attempt: a retry re-partitions only what is left, and a reused chunk id would skip it. const generation = input.acquisitionGeneration ?? `seq-${journal.cursor().sequence}` const settlementId = `stale-session:${input.sessionId}:${input.fence}:${generation}` const mutations: JournalLifecycleMutationInput[] = [] @@ -211,11 +215,24 @@ export async function settleStaleStructuredAgentSessionState(input: { mutations.push({ kind: 'item', identity, body }) } } - mutations.push(...runningTurnLifecycleRevisions(items, verdict)) - if (verdict.state === 'interrupted' && items.some(isInProgressItem)) { + const proven = provenUnverifiableTurnRevisions(items, input.deathEvidence, journal) + mutations.push( + ...items.flatMap((item) => runningTurnLifecycleRevisions([item], verdictFor(item))), + ...proven + ) + const evidence = input.deathEvidence + if ( + evidence && + (proven.length > 0 || + items.some((item) => isInProgressItem(item) && verdictFor(item).state === 'interrupted')) + ) { mutations.unshift({ kind: 'item', - identity: { provider: 'orca', clientMessageId: settlementId }, + // Named by the death it explains, so a retry after a partly written settle adds no second row. + identity: { + provider: 'orca', + clientMessageId: `stale-session:${input.sessionId}:death-${evidence.ownerFence ?? 'unowned'}-${evidence.observedAt}` + }, // The death evidence is Orca's log text, never a sentence for a person: the row says only // that the provider stopped. body: { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink-queue.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink-queue.ts index c9a32533db4..428a3c1c0d4 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink-queue.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink-queue.ts @@ -15,6 +15,8 @@ export type StructuredAgentSessionSinkOperation = { lifecycleBytes?: number lifecycle?: boolean coalescingKey?: string + /** The queued operation with this key wins, as the journal keeps a settlement's first batch. */ + keepsFirst?: boolean run: (target: StructuredAgentSessionEventTarget) => Promise | void } @@ -117,10 +119,13 @@ export class StructuredAgentSessionSinkQueue { if (this.failure !== null) { return { accepted: false, reason: 'failed' } } - const sequence = ++this.acceptedSequence const key = options.coalescingKey ?? operation.coalescingKey const replaceAt = key ? this.queue.findIndex((queued) => queued.coalescingKey === key) : -1 const replaced = replaceAt >= 0 ? this.queue[replaceAt] : undefined + if (replaced && operation.keepsFirst) { + return { accepted: true } + } + const sequence = ++this.acceptedSequence const lifecycle = operation.lifecycle ?? options.lifecycle === true const lifecycleBytes = lifecycle ? (operation.lifecycleBytes ?? operation.bytes) : 0 const nextBytes = this.queuedBytes - (replaced?.bytes ?? 0) + operation.bytes diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts index 6c3747fb824..b8c1d0b7c0e 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts @@ -384,6 +384,32 @@ describe('deferred structured agent-session event sink', () => { expect(log).toEqual([{ call: 'appendItem', fence: 6, ordinal: 1 }]) }) + it('keeps the first queued lifecycle batch for a settlement, as the journal does', async () => { + // The journal applies a settlement id once and skips any later batch with it, + // so the queue must not let a later batch replace one it has not run yet. + const log: Recorded[] = [] + const deferred = createDeferredStructuredAgentSessionEventSink() + const batch = (ordinal: number) => [ + { kind: 'item' as const, identity: identity(ordinal), body: BODY } + ] + + deferred.sink.appendLifecycleBatch?.('turn-completed:turn-1', batch(0)) + expect(deferred.sink.tryAppendLifecycleBatch?.('turn-completed:turn-1', batch(1))).toEqual({ + accepted: true + }) + expect(deferred.state().queuedOperations).toBe(1) + + const bound = target(6, log) + deferred.bind(bound) + await deferred.drained() + + expect( + vi + .mocked(bound.journal.appendLifecycleBatch) + .mock.calls.map(([input]) => input.mutations.map((mutation) => mutation.identity)) + ).toEqual([[identity(0)]]) + }) + it('keeps a replacement checkpoint after distinct intervening operations', async () => { const log: Recorded[] = [] const deferred = createDeferredStructuredAgentSessionEventSink() diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.ts index 923342b7fa6..ad92e1c24ca 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.ts @@ -205,6 +205,7 @@ export function createDeferredStructuredAgentSessionEventSink( { bytes: Buffer.byteLength(JSON.stringify({ settlementId, mutations }), 'utf8') + 512, coalescingKey: `lifecycle:${settlementId}`, + keepsFirst: true, run: (bound) => bound.journal.appendLifecycleBatch({ settlementId, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-eviction.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-eviction.ts index 2c03143ff41..226781117bd 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-eviction.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-eviction.ts @@ -52,7 +52,7 @@ export type StructuredAgentSessionEvictionContext = { } /** The resume offer is advisory; a stalled sink must not hold the child's stop behind it. */ -const SNAPSHOT_DRAIN_TIMEOUT_MS = 1_000 +export const SNAPSHOT_DRAIN_TIMEOUT_MS = 1_000 export type StructuredAgentSessionEvictionStep = { name: string diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts index dace93edc62..d85d1dfee0f 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts @@ -7,6 +7,7 @@ import type { AgentJournalResetReason } from '../../../shared/agent-session-jour import type { AgentSessionJournal } from '../agent-session-journal/journal-store' import { openStructuredAgentSessionConversation, + resettleOpenStructuredAgentSessionConversation, type OpenedStructuredAgentSessionConversation, type StructuredAgentSessionConversationOpenOptions } from './structured-agent-session-conversation-open' @@ -28,6 +29,8 @@ export type StructuredAgentSessionConversationDelivery = { sessionId: string, options?: StructuredAgentSessionConversationOpenOptions ) => Promise + /** Stops the loop and the resettle on a proof of death; quit's first step. */ + dispose: () => void /** Indexes a conversation some other open produced, as `open` would have. */ adoptOpened: ( sessionId: string, @@ -76,9 +79,26 @@ export function createStructuredAgentSessionConversationDelivery(input: { loop.wake(sessionId) } } + // A chat open before its owner's death was proven revises what its open settled. Queued, never + // awaited: the writer can hold this session's serialize (an attach recovering its lease). + const stopResettling = deps.store.onDeathEvidence((sessionId) => { + if (sessions.has(sessionId)) { + void input + .trackStart( + input.serialize(sessionId, () => + resettleOpenStructuredAgentSessionConversation(deps, sessionId, sessions.get(sessionId)) + ) + ) + .catch((error: unknown) => deps.onEventSinkError?.({ sessionId, error })) + } + }) return { loop, adoptOpened, + dispose: () => { + loop.dispose() + stopResettling() + }, open: (sessionId, options) => openStructuredAgentSessionConversation({ deps, sessions, adoptOpened }, sessionId, options) } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.ts index 5fd0440a1fe..1baaafb6f6b 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.ts @@ -1,9 +1,23 @@ import { describe, expect, it, vi } from 'vitest' -import { structuredAgentSessionHostTeardownPhases } from './structured-agent-session-host-teardown' +import { WILL_QUIT_TEARDOWN_DEADLINE_MS } from '../../../shared/quit-teardown-deadline' +import { + GRACEFUL_EXIT_MS as CLAUDE_WINDOWS_GRACEFUL_EXIT_MS, + SUPERVISED_GRACEFUL_EXIT_MS +} from '../../claude/claude-child-exit-proof-ladder' +import { GRACEFUL_EXIT_MS as CODEX_WINDOWS_GRACEFUL_EXIT_MS } from '../../codex/codex-app-server-connection' +import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../codex/codex-app-server-posix-supervisor' +import { SNAPSHOT_DRAIN_TIMEOUT_MS } from './structured-agent-session-eviction' +import { + CHILD_EVICTION_TIMEOUT_MS, + EVICTION_MARGIN_MS, + RESUME_MARKER_RECORD_TIMEOUT_MS, + structuredAgentSessionHostTeardownPhases +} from './structured-agent-session-host-teardown' + +const noop = async (): Promise => undefined describe('structured agent-session host teardown', () => { it('names every phase, so the quit-path order is pinned rather than incidental', () => { - const noop = async (): Promise => undefined const phases = structuredAgentSessionHostTeardownPhases({ idleSweep: { dispose: noop }, runtimeState: { stopLeaseRenewal: () => undefined, flushAllEventSinks: noop }, @@ -23,6 +37,56 @@ describe('structured agent-session host teardown', () => { ]) }) + it("derives quit's child-eviction bound from every provider's supervised close", () => { + // Eviction drains the sink for the resume offer before it stops the child, inside one bound. + // Each close's tree-kill fallback is deliberately outside it: once main exits the supervisor + // stops its group itself, and next launch's recovery settles the lease. + const closes = { + claude: SUPERVISED_GRACEFUL_EXIT_MS, + codex: PROVIDER_SUPERVISOR_MAX_STOP_MS, + // No supervisor on Windows, so its closes wait less than any supervised one. + claudeWindows: CLAUDE_WINDOWS_GRACEFUL_EXIT_MS, + codexWindows: CODEX_WINDOWS_GRACEFUL_EXIT_MS + } + expect(CHILD_EVICTION_TIMEOUT_MS).toBe( + SNAPSHOT_DRAIN_TIMEOUT_MS + Math.max(closes.claude, closes.codex) + EVICTION_MARGIN_MS + ) + for (const closeMs of Object.values(closes)) { + expect(SNAPSHOT_DRAIN_TIMEOUT_MS + closeMs + EVICTION_MARGIN_MS).toBeLessThanOrEqual( + CHILD_EVICTION_TIMEOUT_MS + ) + } + // The resume markers recorded after eviction still fit under quit's global deadline. + expect(CHILD_EVICTION_TIMEOUT_MS + RESUME_MARKER_RECORD_TIMEOUT_MS).toBeLessThan( + WILL_QUIT_TEARDOWN_DEADLINE_MS + ) + }) + + it('ends child eviction as soon as every chat has closed, not at its bound', async () => { + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + const evict = structuredAgentSessionHostTeardownPhases({ + idleSweep: { dispose: noop }, + runtimeState: { stopLeaseRenewal: () => undefined, flushAllEventSinks: noop }, + tasks: { drainAttaches: noop }, + evictOwnedSessions: noop, + beginResumeMarkers: () => {}, + recordResumeMarkers: noop + }).find((phase) => phase.name === 'evict-owned-sessions') + try { + let finished = false + const run = Promise.resolve(evict?.run()).then(() => { + finished = true + }) + // No timer advances: the phase settles with the eviction, not at CHILD_EVICTION_TIMEOUT_MS. + await vi.advanceTimersByTimeAsync(0) + expect(finished).toBe(true) + await run + expect(vi.getTimerCount()).toBe(0) + } finally { + vi.useRealTimers() + } + }) + it('bounds stalled recovery publication without preventing later cleanup', async () => { vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts index 97a27d5c246..bc9a48c5dac 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts @@ -7,6 +7,9 @@ // nothing can ever close them. import type { AgentSessionResumeTrigger } from '../../../shared/agent-session-resume-marker' +import { SUPERVISED_GRACEFUL_EXIT_MS } from '../../claude/claude-child-exit-proof-ladder' +import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../codex/codex-app-server-posix-supervisor' +import { SNAPSHOT_DRAIN_TIMEOUT_MS } from './structured-agent-session-eviction' import type { StructuredAgentSessionRestartResume } from './structured-agent-session-restart-resume-host' import { abandonQueuedStructuredAgentSessionMessages, @@ -23,12 +26,20 @@ export type StructuredAgentSessionTeardownPhase = { } /** Advisory persistence must not hold shutdown open. */ -const RESUME_MARKER_RECORD_TIMEOUT_MS = 2_000 +export const RESUME_MARKER_RECORD_TIMEOUT_MS = 2_000 -/** Eight steps at ten seconds each would outlast the global quit deadline, and a quit that dies - * mid-eviction leaves the lease unreleased — the exact state restart has to clean up. Bounded - * well below that deadline so the phases after this one still get to run. */ -const CHILD_EVICTION_TIMEOUT_MS = 8_000 +/** Covers a provider's stop observed late on a loaded host. */ +export const EVICTION_MARGIN_MS = 1_000 + +/** A quit that dies mid-eviction leaves the lease unreleased — the exact state restart has to + * clean up — so this covers the sink drain plus the longest supervised provider close, well below + * the global quit deadline so later phases still run. A close's tree-kill fallback is outside it: + * once main exits the supervisor stops its group itself, and next launch's recovery settles the + * lease. Windows closes have no supervisor and wait less. */ +export const CHILD_EVICTION_TIMEOUT_MS = + SNAPSHOT_DRAIN_TIMEOUT_MS + + Math.max(SUPERVISED_GRACEFUL_EXIT_MS, PROVIDER_SUPERVISOR_MAX_STOP_MS) + + EVICTION_MARGIN_MS /** Bounds a phase without swallowing its failure, which `withTimeout` alone would. */ async function withPhaseTimeout(run: () => Promise, timeoutMs: number): Promise { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-test-abandon.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-test-abandon.ts index de928eb32e7..a4069875bad 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-test-abandon.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-test-abandon.ts @@ -16,7 +16,7 @@ export async function abandonStructuredAgentSessionHost( host: StructuredAgentSessionHost ): Promise { // First, so the drain below cannot race the loop into enqueueing another step. - host['conversationDelivery'].loop.dispose() + host['conversationDelivery'].dispose() host['lifetime'].dispose() await host['runtimeState'].stopLeaseRenewal() await host['tasks'].drainAttaches() diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts index eba44143074..26697d3f681 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts @@ -247,7 +247,7 @@ export class StructuredAgentSessionHost { // Trigger inlined rather than imported: `AgentSessionResumeTrigger` in shared is the canonical // type, and this file has no line budget left for the import. async flushAllStreamedEvents(options?: { trigger?: 'quit' | 'update' }): Promise { - this.conversationDelivery.loop.dispose() + this.conversationDelivery.dispose() await flushStructuredAgentSessionHost({ ...this.lifetimeContext(), idleSweep: this.lifetime, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts index 7c195a9a124..b1836983869 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts @@ -37,7 +37,17 @@ beforeEach(async () => { resetHostTestOperationIds() lifecycle = [] statuses = [] - const claude = fakeClaude({ initDelayMs: INIT_DELAY_MS, initModel: 'claude-opus-9' }) + // A CLI whose own default is not the catalog's: startup reports it through get_settings, + // since system/init arrives only with the first command. + const claude = fakeClaude({ + initDelayMs: INIT_DELAY_MS, + initModel: 'claude-opus-9', + settings: { + applied: { model: 'claude-opus-9', effort: 'high', advisor: null, ultracode: false }, + effective: { model: 'claude-opus-9', effortLevel: 'high', env: {} }, + sources: {} + } + }) adapter = new ClaudeStructuredSessionAdapter({ resolveLaunch: async () => ({ pathToClaudeCodeExecutable: 'claude', diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.test.ts index 7fbd785894d..063e8c64af7 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.test.ts @@ -1,3 +1,4 @@ +import { spawn } from 'node:child_process' import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -8,6 +9,7 @@ import { writeOlderBuildLease } from '../../runtime/agent-session-older-build-lease.test-fixture' import { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' +import { supervisedPosixLaunch } from '../../codex/codex-app-server-posix-supervisor' import { resolveStructuredSessionRecovery, type StructuredSessionRecoveryResolutionDeps @@ -62,7 +64,7 @@ async function reserve(store: AgentSessionRecordStore) { }) } -async function liveOwner(store: AgentSessionRecordStore) { +async function liveOwner(store: AgentSessionRecordStore, pid = 4242) { const reserved = await reserve(store) const fence = reserved.record.lease.runtimeFence await store.commitProcessIdentity({ @@ -70,7 +72,7 @@ async function liveOwner(store: AgentSessionRecordStore) { fence, process: { hostId: 'local', - pid: 4242, + pid, processStartTimeMs: NOW - 1_000, spawnToken: 'spawn-recovery' }, @@ -302,3 +304,72 @@ describe('structured session recovery resolution', () => { ).toBe('not-applicable') }) }) + +describe.runIf(process.platform !== 'win32')( + 'structured session recovery of a supervised owner', + () => { + const recordedPids: number[] = [] + const alive = (pid: number): boolean => { + try { + process.kill(pid, 0) + return true + } catch (error) { + return !(error instanceof Error && 'code' in error && error.code === 'ESRCH') + } + } + + afterEach(() => { + for (const pid of recordedPids.splice(0)) { + if (alive(pid)) { + process.kill(pid, 'SIGKILL') + } + } + }) + + it('evicts only after the supervisor has reaped a provider that ignores SIGTERM', async () => { + const launch = supervisedPosixLaunch( + { + command: process.execPath, + args: [ + '-e', + "process.on('SIGTERM', () => {}); process.stdout.write(process.pid + '\\n'); setInterval(() => {}, 60000)" + ] + }, + process.env + ) + const supervisor = spawn(launch.command, launch.args, { + env: launch.env, + stdio: ['pipe', 'pipe', 'ignore'], + detached: true + }) + recordedPids.push(supervisor.pid!) + const provider = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('provider never started')), 10_000) + supervisor.stdout.once('data', (chunk: Buffer) => { + clearTimeout(timeout) + resolve(Number(chunk.toString().trim())) + }) + }) + recordedPids.push(provider) + const store = await openStore() + await liveOwner(store, supervisor.pid!) + await latch(store) + + const result = await resolveStructuredSessionRecovery( + { + store, + // The recorded pid is the supervisor's; its absence is the proof recovery evicts on. + probeRecord: async () => + alive(supervisor.pid!) ? MATCHED : { outcome: 'pid-absent' as const }, + now: () => NOW + 10_000 + }, + SESSION + ) + + expect(result).toBe('resolved') + // Evicted on proof of death, which must hold for the provider too, not only the supervisor. + expect(store.getRecord(SESSION)?.lease.deathEvidence).not.toBeNull() + expect(alive(provider)).toBe(false) + }) + } +) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.ts index 5e04cbdf90e..385d438f7a0 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.ts @@ -15,6 +15,7 @@ import { type AgentSessionOwnerProbe } from '../../../shared/agent-session-lease-adjudication' import type { AgentSessionRecord } from '../../../shared/agent-session-record' +import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../codex/codex-app-server-posix-supervisor' import { releaseUnprovenAgentSessionOwner } from '../../runtime/agent-session-lease-transitions' import type { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' @@ -28,8 +29,13 @@ export type StructuredSessionRecoveryResolutionDeps = { delay?: (ms: number) => Promise } -const STOP_PROBES_PER_SIGNAL = 4 const STOP_PROBE_INTERVAL_MS = 250 +// A POSIX structured owner is its provider supervisor, which exits only after its provider +// group. A SIGKILL that lands first leaves the group running, so SIGTERM outlasts its stop. +const STOP_PROBES: Record = { + SIGTERM: Math.ceil(PROVIDER_SUPERVISOR_MAX_STOP_MS / STOP_PROBE_INTERVAL_MS) + 1, + SIGKILL: 4 +} const UNRESOLVED_REFUSALS: ReadonlySet = new Set([ 'agent_session_ownership_unknown', @@ -94,7 +100,7 @@ async function stopOwnerAndReprobe( let probe: AgentSessionOwnerProbe = { outcome: 'indeterminate', reason: 'owner stop requested' } for (const signal of ['SIGTERM', 'SIGKILL'] as const) { stop(pid, signal) - for (let attempt = 0; attempt < STOP_PROBES_PER_SIGNAL; attempt += 1) { + for (let attempt = 0; attempt < STOP_PROBES[signal]; attempt += 1) { probe = await deps.probeRecord(record) if (isProvenDeadProbe(probe)) { return probe diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-send-open-stale-turn.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-send-open-stale-turn.test.ts index a1f196d8b61..0a14f436119 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-send-open-stale-turn.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-send-open-stale-turn.test.ts @@ -86,7 +86,6 @@ async function relaunchAfterCrashMidTurn( return { host, acquire } } -/** Neither proof of the old owner's death is an observed exit, so the turn ends `unverifiable`. */ async function turnStates(host: StructuredAgentSessionHost) { return (await host.journalSnapshot(SESSION)).items .flatMap((item) => readAgentJournalTurn(item.body) ?? []) @@ -94,18 +93,20 @@ async function turnStates(host: StructuredAgentSessionHost) { } // A host that cannot prove the old owner gone leaves its lease in recovery, still claimed, until -// the next acquire resolves it: the open is not that acquire, and the turn is no less gone. +// the next acquire resolves it: the open is not that acquire, and the turn is no less gone. Only +// the proof decides whether it reads interrupted. const PROBES = [ - ['the old owner is proven gone', { outcome: 'pid-absent' }], + ['the old owner is proven gone', { outcome: 'pid-absent' }, 'interrupted'], [ 'nothing proves the old owner gone', - { outcome: 'indeterminate', reason: 'This host cannot probe structured session owners.' } + { outcome: 'indeterminate', reason: 'This host cannot probe structured session owners.' }, + 'unverifiable' ] -] as const satisfies readonly (readonly [string, AgentSessionOwnerProbe])[] +] as const satisfies readonly (readonly [string, AgentSessionOwnerProbe, string])[] it.each(PROBES)( 'settles a turn a dead generation left running when a send opens the chat and its start fails, when %s', - async (_when, probe) => { + async (_when, probe, settled) => { const { host, acquire } = await relaunchAfterCrashMidTurn(probe) expect(host.hasSession(SESSION)).toBe(false) @@ -123,32 +124,34 @@ it.each(PROBES)( ) expect(acquire).toHaveBeenCalledOnce() - expect(await turnStates(host)).toEqual(['unverifiable']) + expect(await turnStates(host)).toEqual([settled]) await host.flushAllStreamedEvents() } ) it.each(PROBES)( 'settles the same turn when a reader opens the chat, when %s', - async (_when, probe) => { + async (_when, probe, settled) => { const { host } = await relaunchAfterCrashMidTurn(probe) await host.revealSession(SESSION) - expect(await turnStates(host)).toEqual(['unverifiable']) + expect(await turnStates(host)).toEqual([settled]) await host.flushAllStreamedEvents() } ) -// On desktop the chat on screen at relaunch reads before startup reconciles the leases. -it('settles it when a read reaches the chat before the startup reconcile', async () => { +// On desktop the chat on screen at relaunch reads before startup reconciles the leases: nothing +// has proved its owner gone yet, and the reconcile's proof then revises what the open settled. +it('settles it when a read reaches the chat before the startup reconcile, then revises it', async () => { const { host } = await relaunchAfterCrashMidTurn({ outcome: 'pid-absent' }, { reconcile: false }) expect(hostTestState().store.getRecord(SESSION)?.lease.claimStatus).toBe('live') await host.history({ sessionId: SESSION, direction: 'tail' }) + expect(await turnStates(host)).toEqual(['unverifiable']) await host.reconcileRestartLeases() await host.restoreReadableSessions([SESSION]) - expect(await turnStates(host)).toEqual(['unverifiable']) + expect(await turnStates(host)).toEqual(['interrupted']) await host.flushAllStreamedEvents() }) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-open.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-open.test.ts index 5598cd3c4f7..83959233f6f 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-open.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-open.test.ts @@ -28,8 +28,9 @@ afterEach(async () => { await rig.dispose() }) -/** A turn in flight whose agent exits, and whose exit settlement the journal refuses. */ -async function exitWithUnwrittenSettlement(): Promise { +/** A turn in flight whose agent exits, and whose exit settlement the journal refuses: by default + * also the retry that recording the exit queues, so only an open is left to settle it. */ +async function exitWithUnwrittenSettlement(refusedWrites = 2): Promise { await foundRestTestChat(rig) const open = rig.host.collaboratorsForTests().sessions.get(SESSION)! const running = open.child! @@ -40,7 +41,10 @@ async function exitWithUnwrittenSettlement(): Promise { { kind: 'turn', turnId: 'working', state: 'running' } ) await rig.host.flushStreamedEvents(SESSION) - vi.spyOn(open.journal, 'appendLifecycleBatch').mockRejectedValueOnce(new Error('disk full')) + const append = vi.spyOn(open.journal, 'appendLifecycleBatch') + for (let refused = 0; refused < refusedWrites; refused += 1) { + append.mockRejectedValueOnce(new Error('disk full')) + } await rig.host.handleAdapterEvent({ type: 'ended', sessionId: SESSION, @@ -55,6 +59,7 @@ async function exitWithUnwrittenSettlement(): Promise { deathEvidence: { kind: 'exit-observed' } }) ) + await rig.host.collaboratorsForTests().serialize(SESSION, async () => {}) } async function workingTurnState(): Promise { @@ -65,6 +70,18 @@ async function workingTurnState(): Promise { } describe('a turn its gone agent left running', () => { + it('is settled in place by the retry recording the exit queues, with no reopen', async () => { + await exitWithUnwrittenSettlement(1) + + expect(rig.host.collaboratorsForTests().sessions.has(SESSION)).toBe(true) + expect(await workingTurnState()).toBe('interrupted') + const { items } = await rig.host.journalSnapshot(SESSION) + expect(JSON.stringify(items)).toContain( + 'Codex stopped while this response was in progress. You can continue in this conversation.' + ) + expect(rig.adapter.acquire).toHaveBeenCalledOnce() + }) + it('is settled when the idle-closed conversation is opened again, and its reader sees it', async () => { await exitWithUnwrittenSettlement() expect(await workingTurnState()).toBe('running') diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts index 4ab7b3ad010..2c7a91e7637 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts @@ -4,6 +4,8 @@ import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { agentJournalItemKey } from '../../../shared/agent-session-journal-item-key' import type { AgentJournalRenderItem } from '../../../shared/agent-session-journal-types' +import type { AgentSessionDeathEvidence } from '../../../shared/agent-session-record' +import { readAgentJournalTurn } from '../../../shared/agent-session-turn-record' import type { AgentSessionJournal } from '../agent-session-journal/journal-store' import { createTrackedJournalOpener } from '../agent-session-journal/journal-store-test-open' import { settleStaleStructuredAgentSessionState } from './structured-agent-session-dead-generation-settlement' @@ -75,17 +77,77 @@ function promptItem(state: 'pending' | 'resolved', sequence: number): AgentJourn } describe('turn verdict from death evidence', () => { - it('earns an end time only from an observed exit', () => { + /** Judges a turn the fence-1 owner wrote, by evidence naming that owner. */ + const verdictForTurn = (evidence: AgentSessionDeathEvidence | null | undefined) => + turnVerdictFromDeathEvidence(evidence && { ownerFence: 1, ...evidence }, 1) + + it('ends a watched exit at the exit', () => { + expect(verdictForTurn({ kind: 'exit-observed', detail: 'exit', observedAt: 500 })).toEqual({ + state: 'interrupted', + completedAt: 500 + }) + }) + + it.each(['pid-absent', 'identity-mismatch'] as const)( + 'ends a %s proof at the last proof of life, never after the probe', + (kind) => { + const proof = (lastProvenAliveAt?: number) => ({ + kind, + detail: 'gone', + observedAt: 9_000, + ...(lastProvenAliveAt === undefined ? {} : { lastProvenAliveAt }) + }) + // Probed at 9000, long after the crash: the downtime is never counted as work. + expect(verdictForTurn(proof(8_000))).toEqual({ state: 'interrupted', completedAt: 8_000 }) + expect(verdictForTurn(proof(9_500))).toEqual({ state: 'interrupted', completedAt: 9_000 }) + // A proof that recorded no proof of life has only the probe. + expect(verdictForTurn(proof())).toEqual({ state: 'interrupted', completedAt: 9_000 }) + } + ) + + it('ends a watched exit at the exit even when a renewal is recorded', () => { expect( - turnVerdictFromDeathEvidence({ kind: 'exit-observed', detail: 'exit', observedAt: 500 }) + verdictForTurn({ + kind: 'exit-observed', + detail: 'exit', + observedAt: 500, + lastProvenAliveAt: 400 + }) ).toEqual({ state: 'interrupted', completedAt: 500 }) - expect( - turnVerdictFromDeathEvidence({ kind: 'pid-absent', detail: 'gone', observedAt: 500 }) - ).toEqual({ state: 'unverifiable' }) - expect( - turnVerdictFromDeathEvidence({ kind: 'identity-mismatch', detail: 'pid', observedAt: 500 }) - ).toEqual({ state: 'unverifiable' }) - expect(turnVerdictFromDeathEvidence(null)).toEqual({ state: 'unverifiable' }) + }) + + it('leaves a release nothing proved unverifiable', () => { + expect(verdictForTurn(null)).toEqual({ state: 'unverifiable' }) + expect(verdictForTurn(undefined)).toEqual({ state: 'unverifiable' }) + }) + it('keeps the rule an older build applied to evidence that names no owner', () => { + // Only a watched exit was proof then; a probe's proof stays unverifiable. + const legacy = { detail: 'gone', observedAt: 9_000, lastProvenAliveAt: 8_000 } + expect(turnVerdictFromDeathEvidence({ ...legacy, kind: 'exit-observed' }, 1)).toEqual({ + state: 'interrupted', + completedAt: 9_000 + }) + expect(turnVerdictFromDeathEvidence({ ...legacy, kind: 'pid-absent' }, 1)).toEqual({ + state: 'unverifiable' + }) + }) + + it('gives a turn no end from evidence about any other owner', () => { + const proof = { + kind: 'pid-absent' as const, + detail: 'gone', + observedAt: 9_000, + lastProvenAliveAt: 8_000 + } + // A newer start's death, and a turn whose writer the timeline no longer knows: neither proves + // the turn's own owner gone. + for (const [evidence, turnFence] of [ + [{ ...proof, ownerFence: 3 }, 1], + [{ ...proof, kind: 'exit-observed' as const, ownerFence: 3 }, 1], + [{ ...proof, ownerFence: 1 }, undefined] + ] as const) { + expect(turnVerdictFromDeathEvidence(evidence, turnFence)).toEqual({ state: 'unverifiable' }) + } }) }) @@ -162,6 +224,13 @@ describe('running turn lifecycle revisions', () => { }) }) + it('never ends a turn before it began, when the last proof of life predates it', () => { + const item = lifecycleItem('turn-2', 'running', 2, { startedAt: 500 }) + expect( + runningTurnLifecycleRevisions([item], { state: 'interrupted', completedAt: 300 }) + ).toMatchObject([{ body: { kind: 'turn', state: 'interrupted', completedAt: 500 } }]) + }) + it('revises a legacy status-form running row from an older host into a typed turn', () => { expect( runningTurnLifecycleRevisions([legacyLifecycleItem('turn-2', 30)], { state: 'unverifiable' }) @@ -180,11 +249,22 @@ describe('running turn lifecycle revisions', () => { }) }) +/** A probe that found the fence-1 owner gone at 9000, last proven alive at 100. */ +const PROVEN: AgentSessionDeathEvidence = { + kind: 'pid-absent', + detail: 'recorded pid absent on host', + observedAt: 9_000, + ownerFence: 1, + lastProvenAliveAt: 100 +} + describe('stale session state on a cold acquire', () => { function journalWith(items: AgentJournalRenderItem[]) { const appendLifecycleBatch = vi.fn(async () => ({ epoch: 'epoch-1', sequence: 9 })) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the settle reads only these journal members. const journal = { snapshot: () => ({ items }), + itemFence: () => 1, cursor: () => ({ epoch: 'epoch-1', sequence: 8 }), appendLifecycleBatch } as unknown as AgentSessionJournal @@ -310,6 +390,255 @@ describe('stale session state on a cold acquire', () => { } }) + it('ends a probe-proven turn at its last proof of life, not at a row written after it', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-stale-session-')) + const journals = createTrackedJournalOpener() + let now = 100 + try { + const journal = await journals.open({ + identity: { + sessionId: 'session-1', + workspaceId: 'workspace-1', + hostId: 'local', + agent: 'codex', + providerHandle: { kind: 'codex', threadId: THREAD } + }, + journalDir: root, + now: () => now + }) + const command = { provider: 'codex' as const, threadId: THREAD, turnId: 'turn-1', ordinal: 1 } + const shell = { kind: 'tool-call' as const, name: 'shell', input: { command: 'pnpm test' } } + await journal.appendItem( + { ...command, ordinal: 0 }, + { kind: 'turn', turnId: 'turn-1', state: 'running', startedAt: 100 }, + { fence: 1 } + ) + now = 200 + await journal.appendItem(command, { ...shell, state: 'running' }, { fence: 1 }) + // Rows can outlast the last renewal; only the renewal is proof of life. + now = 700 + await journal.appendItem( + command, + { ...shell, input: { command: 'pnpm test', streamed: 'ok' }, state: 'running' }, + { fence: 1 } + ) + now = 9_000 + + await settleStaleStructuredAgentSessionState({ + journal, + sessionId: 'session-1', + fence: 2, + acquisitionGeneration: 'generation-2', + deathEvidence: { + kind: 'pid-absent', + detail: 'recorded pid absent on host', + observedAt: 9_000, + ownerFence: 1, + lastProvenAliveAt: 650 + } + }) + + const items = journal.snapshot().items + expect(items.map((item) => readAgentJournalTurn(item.body)).find(Boolean)).toMatchObject({ + state: 'interrupted', + completedAt: 650 + }) + // The probe's detail is Orca's, so the row carries none. + expect( + items.flatMap((item) => (item.body.kind === 'status' ? [item.body.text] : [])) + ).toEqual([ + 'The agent stopped while this response was in progress. You can continue in this conversation.' + ]) + } finally { + await journals.closeAll() + await rm(root, { recursive: true, force: true }) + } + }) + + it('ends a crashed turn at its last proof of life, not at a send accepted before the proof', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-stale-session-')) + const journals = createTrackedJournalOpener() + let now = 100 + try { + const journal = await journals.open({ + identity: { + sessionId: 'session-1', + workspaceId: 'workspace-1', + hostId: 'local', + agent: 'codex', + providerHandle: { kind: 'codex', threadId: THREAD } + }, + journalDir: root, + now: () => now + }) + await journal.appendItem( + RUNNING_IDENTITY, + { kind: 'turn', turnId: 'turn-2', state: 'running', startedAt: 100 }, + { fence: 1 } + ) + now = 200 + await journal.appendItem( + { ...RUNNING_IDENTITY, ordinal: 1 }, + { kind: 'tool-call', name: 'shell', input: { command: 'pnpm test' }, state: 'running' }, + { fence: 1 } + ) + const settle = (deathEvidence: AgentSessionDeathEvidence | null) => + settleStaleStructuredAgentSessionState({ + journal, + sessionId: 'session-1', + fence: 1, + acquisitionGeneration: null, + deathEvidence + }) + // An hour later the relaunch opens the chat before anything proved the owner gone. + now = 3_600_000 + await settle(null) + const turn = () => journal.snapshot().items.map((item) => readAgentJournalTurn(item.body))[0] + expect(turn()).toMatchObject({ state: 'unverifiable' }) + now = 3_605_000 + await journal.appendSubmission({ + clientMessageId: 'send-1', + payloadFingerprint: 'fingerprint-1', + body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'still there?' }] }, + fence: 1, + handoverRecorded: true + }) + await journal.resolveDispatch({ clientMessageId: 'send-1', state: 'pending', fence: 1 }) + now = 3_606_000 + + await settle({ + kind: 'pid-absent', + detail: 'gone', + observedAt: 3_606_000, + ownerFence: 1, + lastProvenAliveAt: 210 + }) + + expect(turn()).toMatchObject({ state: 'interrupted', completedAt: 210 }) + } finally { + await journals.closeAll() + await rm(root, { recursive: true, force: true }) + } + }) + + it('revises an unverifiable turn only from a proof naming its own owner, and only once', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-stale-session-')) + const journals = createTrackedJournalOpener() + let now = 100 + try { + const journal = await journals.open({ + identity: { + sessionId: 'session-1', + workspaceId: 'workspace-1', + hostId: 'local', + agent: 'codex', + providerHandle: { kind: 'codex', threadId: THREAD } + }, + journalDir: root, + now: () => now + }) + await journal.appendItem( + RUNNING_IDENTITY, + { kind: 'turn', turnId: 'turn-2', state: 'running', startedAt: 100 }, + { fence: 1 } + ) + now = 400 + // The open, before anything proved the fence-1 owner gone. + const settle = (deathEvidence: AgentSessionDeathEvidence | null) => + settleStaleStructuredAgentSessionState({ + journal, + sessionId: 'session-1', + fence: 2, + acquisitionGeneration: null, + deathEvidence + }) + await settle(null) + now = 9_000 + const turn = () => journal.snapshot().items.map((item) => readAgentJournalTurn(item.body))[0] + expect(turn()).toMatchObject({ state: 'unverifiable' }) + + const unrevised = journal.cursor() + await expect(settle({ ...PROVEN, ownerFence: 2 })).resolves.toBe(0) + const { ownerFence: _ownerFence, ...olderBuildProof } = PROVEN + await expect(settle({ ...olderBuildProof, kind: 'exit-observed' })).resolves.toBe(0) + expect(journal.cursor()).toEqual(unrevised) + expect(turn()).toMatchObject({ state: 'unverifiable' }) + + await expect(settle(PROVEN)).resolves.toBe(2) + expect(turn()).toEqual({ + turnId: 'turn-2', + state: 'interrupted', + startedAt: 100, + completedAt: 100 + }) + const revised = journal.cursor() + await expect(settle(PROVEN)).resolves.toBe(0) + expect(journal.cursor()).toEqual(revised) + expect(journal.snapshot().items.filter((item) => item.body.kind === 'status')).toHaveLength(1) + } finally { + await journals.closeAll() + await rm(root, { recursive: true, force: true }) + } + }) + + it('adds one row for a death however many attempts its settle takes to write', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-stale-session-')) + const journals = createTrackedJournalOpener() + let now = 100 + try { + const journal = await journals.open({ + identity: { + sessionId: 'session-1', + workspaceId: 'workspace-1', + hostId: 'local', + agent: 'codex', + providerHandle: { kind: 'codex', threadId: THREAD } + }, + journalDir: root, + now: () => now + }) + // Enough running turns that the settle writes two batches, and its retry two again. + for (let index = 0; index < 399; index++) { + const turnId = `turn-${index}` + await journal.appendItem( + { ...RUNNING_IDENTITY, turnId }, + { kind: 'turn', turnId, state: 'running', startedAt: 100 }, + { fence: 1 } + ) + } + now = 9_000 + const settle = () => + settleStaleStructuredAgentSessionState({ + journal, + sessionId: 'session-1', + fence: 2, + acquisitionGeneration: null, + deathEvidence: PROVEN + }) + const turns = () => + journal.snapshot().items.flatMap((item) => readAgentJournalTurn(item.body) ?? []) + const statusRows = () => + journal.snapshot().items.filter((item) => item.body.kind === 'status') + const append = journal.appendLifecycleBatch.bind(journal) + const secondBatchFails = vi + .spyOn(journal, 'appendLifecycleBatch') + .mockImplementationOnce(append) + .mockRejectedValueOnce(new Error('disk full')) + + await expect(settle()).rejects.toThrow('disk full') + secondBatchFails.mockRestore() + expect(turns().filter((turn) => turn.state === 'running')).toHaveLength(200) + expect(statusRows()).toHaveLength(1) + + await settle() + expect(turns().filter((turn) => turn.state !== 'interrupted')).toEqual([]) + expect(statusRows()).toHaveLength(1) + } finally { + await journals.closeAll() + await rm(root, { recursive: true, force: true }) + } + }) + it('writes nothing when no turn is running and keys on the journal position without a generation', async () => { const idle = journalWith([ lifecycleItem('turn-1', 'completed', 1, { startedAt: 10, completedAt: 20 }) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.ts index b6e9b9bde5e..b23018aeba8 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.ts @@ -1,8 +1,10 @@ // What the host may durably say about a turn whose provider child is gone. // -// `interrupted` requires the host to have seen the child exit; that receipt is the only end time it -// is allowed to record. Everything weaker — a pid probe, an identity mismatch, a journal found -// running with no observed exit on the record — is `unverifiable` and carries no end at all. +// `interrupted` requires proof that the child which wrote the turn is gone: death evidence naming +// that turn's owner by fence — a watched exit, or a local probe that found the recorded pid gone or +// reused. A release nothing proved — lost contact, an unverifiable identity, a stop that outlived the +// ladder — carries none, and neither does a later owner's death; the turn is then `unverifiable` +// with no end at all, until a proof naming its owner is written and revises it. import { parseAgentJournalItemKey } from '../../../shared/agent-session-journal-item-key' import type { @@ -15,6 +17,7 @@ import { } from '../../../shared/agent-session-turn-record' import type { AgentSessionDeathEvidence } from '../../../shared/agent-session-record' import type { JournalLifecycleMutationInput } from '../agent-session-journal/journal-row-builders' +import type { AgentSessionJournal } from '../agent-session-journal/journal-store' export type StructuredAgentSessionTurnVerdict = | { state: 'interrupted'; completedAt: number } @@ -25,11 +28,31 @@ export const UNVERIFIABLE_TURN_VERDICT: StructuredAgentSessionTurnVerdict = { } export function turnVerdictFromDeathEvidence( - evidence: AgentSessionDeathEvidence | null | undefined + evidence: AgentSessionDeathEvidence | null | undefined, + /** Fence of the owner that wrote the turn. */ + turnFence: number | undefined ): StructuredAgentSessionTurnVerdict { - return evidence?.kind === 'exit-observed' - ? { state: 'interrupted', completedAt: evidence.observedAt } - : UNVERIFIABLE_TURN_VERDICT + if (!evidence) { + return UNVERIFIABLE_TURN_VERDICT + } + if (evidence.ownerFence === undefined) { + // Evidence an older build wrote names no owner; it keeps the rule that build applied. + return evidence.kind === 'exit-observed' + ? { state: 'interrupted', completedAt: evidence.observedAt } + : UNVERIFIABLE_TURN_VERDICT + } + if (evidence.ownerFence !== turnFence) { + return UNVERIFIABLE_TURN_VERDICT + } + if (evidence.kind === 'exit-observed') { + return { state: 'interrupted', completedAt: evidence.observedAt } + } + // A probe finds a dead child long after it died; its last renewal bounds the end, so the turn never + // counts the time Orca was down. Timeline rows don't: a send can land there after the death. + return { + state: 'interrupted', + completedAt: Math.min(evidence.lastProvenAliveAt ?? evidence.observedAt, evidence.observedAt) + } } /** Revises every still-running lifecycle item in place, keeping its identity and start. */ @@ -37,23 +60,43 @@ export function runningTurnLifecycleRevisions( items: readonly AgentJournalRenderItem[], verdict: StructuredAgentSessionTurnVerdict ): JournalLifecycleMutationInput[] { - const revisions: JournalLifecycleMutationInput[] = [] - for (const item of items) { + return items.flatMap((item) => { const turn = readAgentJournalTurn(item.body) - if (turn?.state !== 'running') { - continue - } - const identity = parseAgentJournalItemKey(item.itemId) - if (!identity) { - continue - } - revisions.push({ - kind: 'item', - identity, - body: agentJournalTurnBody(settledLifecycle(turn, verdict)) - }) + return turn?.state === 'running' ? turnLifecycleRevision(item, turn, verdict) : [] + }) +} + +/** + * A turn an earlier settle could only call `unverifiable`, because the proof had not been written + * yet, revised once a proof names the owner that wrote it. Only ever upward, and never from an + * older build's proof, which names no owner. + */ +export function provenUnverifiableTurnRevisions( + items: readonly AgentJournalRenderItem[], + evidence: AgentSessionDeathEvidence | null | undefined, + journal: Pick +): JournalLifecycleMutationInput[] { + const ownerFence = evidence?.ownerFence + if (ownerFence === undefined) { + return [] } - return revisions + return items.flatMap((item) => { + const turn = readAgentJournalTurn(item.body) + return turn?.state === 'unverifiable' && journal.itemFence(item.itemId) === ownerFence + ? turnLifecycleRevision(item, turn, turnVerdictFromDeathEvidence(evidence, ownerFence)) + : [] + }) +} + +function turnLifecycleRevision( + item: AgentJournalRenderItem, + turn: AgentJournalTurnLifecycle, + verdict: StructuredAgentSessionTurnVerdict +): JournalLifecycleMutationInput[] { + const identity = parseAgentJournalItemKey(item.itemId) + return identity + ? [{ kind: 'item', identity, body: agentJournalTurnBody(settledLifecycle(turn, verdict)) }] + : [] } /** The verdict owns the turn's end and nothing else; every other field the row @@ -69,7 +112,10 @@ function settledLifecycle( durationMs: _durationMs, ...kept } = lifecycle - return verdict.state === 'interrupted' - ? { ...kept, state: verdict.state, completedAt: verdict.completedAt } - : { ...kept, state: verdict.state } + if (verdict.state !== 'interrupted') { + return { ...kept, state: verdict.state } + } + // A renewal can predate the turn, which started with its owner alive; it never ends before that. + const began = Math.max(lifecycle.requestedAt ?? 0, lifecycle.startedAt ?? 0) + return { ...kept, state: verdict.state, completedAt: Math.max(verdict.completedAt, began) } } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child.test.ts new file mode 100644 index 00000000000..78e10bec076 --- /dev/null +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child.test.ts @@ -0,0 +1,76 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, expect, it } from 'vitest' +import type { AgentSessionStatusEvent } from '../../../shared/agent-session-wire' +import { createTrackedJournalOpener } from '../agent-session-journal/journal-store-test-open' +import { StructuredAgentSessionStatusFeed } from './structured-agent-session-status-feed' +import { indexedStatusFeedSession } from './structured-agent-session-status-feed-test-session' + +const SESSION = 'status-session' +const journals = createTrackedJournalOpener() +let root: string + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-status-child-')) +}) + +afterEach(async () => { + await journals.closeAll() + await rm(root, { recursive: true, force: true }) +}) + +it('publishes each provider child even when a replacement has the same startup phase', async () => { + const journal = await journals.open({ + identity: { + sessionId: SESSION, + workspaceId: 'workspace-1', + hostId: 'local', + agent: 'codex', + providerHandle: { kind: 'codex', threadId: 'thread-1' } + }, + journalDir: join(root, SESSION) + }) + const sessions = new Map>() + const setChild = ( + child: { + phase: 'starting' | 'ready' + generation: string + fence: number + } | null + ) => sessions.set(SESSION, indexedStatusFeedSession({ journal, child })) + setChild({ phase: 'starting', generation: 'child-1', fence: 1 }) + const events: AgentSessionStatusEvent[] = [] + const feed = new StructuredAgentSessionStatusFeed({ + sessions, + getRecord: () => null, + now: () => 1 + }) + const dispose = feed.subscribe({ id: 'list-1', emit: (event) => events.push(event) }) + expect(events.at(-1)).toMatchObject({ + type: 'snapshot', + sessions: [ + { + hostExecutionOwned: true, + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: 'child-1', fence: 1 } + } + ] + }) + setChild({ phase: 'starting', generation: 'child-2', fence: 2 }) + feed.publish(SESSION, journal) + expect(events.at(-1)).toMatchObject({ + session: { + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: 'child-2', fence: 2 } + } + }) + setChild({ phase: 'ready', generation: 'child-2', fence: 2 }) + feed.publish(SESSION, journal) + expect(events.at(-1)).toMatchObject({ session: { hostExecutionPhase: 'ready' } }) + setChild(null) + feed.publish(SESSION, journal) + expect(events.at(-1)).not.toMatchObject({ session: { hostExecutionPhase: expect.any(String) } }) + expect(events.at(-1)).not.toMatchObject({ session: { hostExecutionChild: expect.any(Object) } }) + dispose() +}) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-test-session.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-test-session.ts index c130aaf9d59..da7275cc641 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-test-session.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-test-session.ts @@ -1,8 +1,9 @@ import type { AgentSessionJournal } from '../agent-session-journal/journal-store' +import type { StructuredAgentSessionProviderChildIdentity } from './structured-agent-session-host-types' export function indexedStatusFeedSession(session: { journal: AgentSessionJournal - child?: { phase: 'starting' | 'ready' } | null + child?: (StructuredAgentSessionProviderChildIdentity & { phase: 'starting' | 'ready' }) | null }) { return { journal: session.journal, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts index a26cd324217..eda619694e3 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts @@ -95,27 +95,11 @@ function feedFor( } describe('StructuredAgentSessionStatusFeed', () => { - it('projects whether the owned child has proven its start, and nothing once it is not owned', async () => { - const journal = await openJournal() - const session = { journal, child: { phase: 'starting' as const } } - const sessions = new Map[0]>([[SESSION, session]]) - const { feed, events, dispose } = feedFor(sessions) - expect(events.at(-1)).toMatchObject({ - type: 'snapshot', - sessions: [{ hostExecutionOwned: true, hostExecutionPhase: 'starting' }] - }) - sessions.set(SESSION, { ...session, child: { phase: 'ready' } }) - feed.publish(SESSION, journal) - expect(events.at(-1)).toMatchObject({ session: { hostExecutionPhase: 'ready' } }) - sessions.set(SESSION, { ...session, child: null }) - feed.publish(SESSION, journal) - expect(events.at(-1)).not.toMatchObject({ session: { hostExecutionPhase: expect.any(String) } }) - dispose() - }) - it('publishes provider ownership transitions without changing journal time', async () => { const journal = await openJournal() - const sessions = new Map([[SESSION, { journal, child: { phase: 'ready' } }]]) + const sessions = new Map([ + [SESSION, { journal, child: { phase: 'ready', generation: 'child-1', fence: 1 } }] + ]) const { feed, events, dispose } = feedFor(sessions) events.length = 0 await journal.appendItem( @@ -800,7 +784,9 @@ describe('the status sink sees the roster the broadcast cache deliberately lacks it('receives every change once, ownership revocation, and the forget edge', async () => { const journal = await openJournal() - const sessions = new Map([[SESSION, { journal, child: { phase: 'ready' } }]]) + const sessions = new Map([ + [SESSION, { journal, child: { phase: 'ready', generation: 'child-1', fence: 1 } }] + ]) const { sink, published, forgotten } = sinkFor() const { feed } = feedFor(sessions, null, undefined, undefined, sink) await journal.appendItem( @@ -822,6 +808,8 @@ describe('the status sink sees the roster the broadcast cache deliberately lacks feed.revokeLive(SESSION) expect(published.at(-1)).toMatchObject({ sessionId: SESSION, status: 'idle' }) expect(published.at(-1)?.hostExecutionOwned).toBeUndefined() + expect(published.at(-1)?.hostExecutionPhase).toBeUndefined() + expect(published.at(-1)?.hostExecutionChild).toBeUndefined() // Exactly what `close` does after eviction: the cache keeps the projection, the sink does not. sessions.delete(SESSION) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts index 05937aca514..22a61a006bb 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts @@ -25,7 +25,7 @@ import type { AgentChildWorkEvidence } from '../../../shared/agent-status-child- import { projectStructuredAgentSessionStatusState } from '../../../shared/structured-agent-session-projection' import { structuredAgentSessionAgentStatus } from '../../../shared/structured-agent-session-agent-status' import type { AgentSessionJournal } from '../agent-session-journal/journal-store' -import type { StructuredAgentSessionProviderChildPhase } from './structured-agent-session-adapter' +import type { StructuredAgentSessionProviderChild } from './structured-agent-session-host-types' import { structuredAgentSessionProviderSessionMetadata } from './structured-agent-session-history-result' import { StructuredAgentSessionStatusOwnership, @@ -46,7 +46,7 @@ export type StructuredAgentSessionStatusSubscriber = { type StatusFeedSession = { journal: AgentSessionJournal params: { location: AgentSessionRecord['location']; provider: AgentSessionRecord['provider'] } - child?: { phase: StructuredAgentSessionProviderChildPhase } | null + child?: Pick | null } export type StructuredAgentSessionStatusFeedDeps = { @@ -73,6 +73,8 @@ function summariesEqual(a: AgentSessionStatusSummary, b: AgentSessionStatusSumma a.status === b.status && a.hostExecutionOwned === b.hostExecutionOwned && a.hostExecutionPhase === b.hostExecutionPhase && + a.hostExecutionChild?.generation === b.hostExecutionChild?.generation && + a.hostExecutionChild?.fence === b.hostExecutionChild?.fence && a.rewindBlockedReason === b.rewindBlockedReason && // A moved state clock changes ranking; row activity alone, including a subagent's, does not. // An idle state the journal cannot date still republishes, since readers date it by `updatedAt`, @@ -203,7 +205,12 @@ export class StructuredAgentSessionStatusFeed { if (!previous) { return } - const { hostExecutionOwned: _hostExecutionOwned, ...retained } = previous + const { + hostExecutionOwned: _hostExecutionOwned, + hostExecutionPhase: _hostExecutionPhase, + hostExecutionChild: _hostExecutionChild, + ...retained + } = previous this.published.set(sessionId, retained) this.sink(retained) this.broadcast({ @@ -273,7 +280,11 @@ export class StructuredAgentSessionStatusFeed { workspaceId: session.params.location.workspaceId, agent: session.params.provider, ...(session.child - ? { hostExecutionOwned: true as const, hostExecutionPhase: session.child.phase } + ? { + hostExecutionOwned: true as const, + hostExecutionPhase: session.child.phase, + hostExecutionChild: { generation: session.child.generation, fence: session.child.fence } + } : {}), ...projected, ...(record?.rewind?.phase === 'prepared' || record?.rewind?.phase === 'provider-succeeded' diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-subagent-recency.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-subagent-recency.test.ts index 4053988233f..cb8b3d7d7c5 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-subagent-recency.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-subagent-recency.test.ts @@ -64,7 +64,13 @@ async function openSession() { const server = new AgentHookServer() const feed = new StructuredAgentSessionStatusFeed({ sessions: new Map([ - [SESSION, indexedStatusFeedSession({ journal, child: { phase: 'ready' } })] + [ + SESSION, + indexedStatusFeedSession({ + journal, + child: { phase: 'ready', generation: 'child-1', fence: 1 } + }) + ] ]), getRecord: () => null, now: () => 1, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts index 98e50783bdf..b04a9b3d820 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts @@ -767,7 +767,9 @@ describe('an unexpected provider exit', () => { runtimeFence: exitedFence + 1, deathEvidence: { kind: 'exit-observed', detail: 'provider exited', observedAt: NOW } }) - // Nothing retries the settlement; the journal writes again, and the next acquire re-derives it. + // The retry recording the exit queues is refused too; once the journal writes again, the next + // acquire re-derives it. + await host.collaboratorsForTests().serialize(SESSION, async () => {}) refusing.mockRestore() dispatch.mockResolvedValueOnce({ diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts index 0685af60549..451629f7b45 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts @@ -96,6 +96,7 @@ describe('provider-exit settlement', () => { snapshot: () => ({ items: [lifecycleItem('turn-1', 1, { state: 'running', startedAt: 1_000 })] }), + itemFence: () => 7, appendLifecycleBatch, markPendingSubmissionsUnknown: vi.fn(async () => []) } @@ -123,7 +124,7 @@ describe('provider-exit settlement', () => { observedAt } ) - // Released, not latched: the next acquire or read restore settles what this write left. + // Released, not latched: a later settle from this evidence finishes what this write left. expect(record.lease).toMatchObject({ claimStatus: 'released', handoffStage: null, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.ts index 3a47d1240f2..5025c5a6f82 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.ts @@ -131,8 +131,8 @@ export async function settleUnexpectedStructuredAgentSessionExit< acquisitionGeneration: child.generation, now: context.now(), exitObservedAt: observedAt, - // Bare cause: whatever this settlement could not write is settled from it later, by the - // next acquire or read restore, and `exit-observed` already says the rest. + // Bare cause: whatever this settlement could not write is settled from it later (the + // settle recording it queues, or the next open or acquire); `exit-observed` says the rest. exitReason: unexpectedEvent.reason.slice(0, MAX_UNEXPECTED_EXIT_REASON_CHARS) }) } catch (error) { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts index 98b212a8606..a492b0b65ce 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts @@ -191,6 +191,9 @@ function isAcquirable(lease: NonNullable>['le ) } +/** After the owner's last renewal, so a turn it proves dead ends at its start, never before. */ +const SEEDED_TURN_STARTED_AT = NOW - 5_000 + async function seedRunningTurn(provider: 'codex' | 'claude' = 'codex'): Promise { const journal = await openAgentSessionJournal({ identity: { @@ -209,7 +212,7 @@ async function seedRunningTurn(provider: 'codex' | 'claude' = 'codex'): Promise< provider === 'codex' ? { provider: 'codex', threadId: THREAD, turnId: 'turn-1', ordinal: 0 } : { provider: 'claude', sessionId: 'provider-session-alpha-1', uuid: 'uuid-running' }, - { kind: 'turn', turnId: 'turn-1', state: 'running', startedAt: NOW - 5_000 }, + { kind: 'turn', turnId: 'turn-1', state: 'running', startedAt: SEEDED_TURN_STARTED_AT }, { fence: 13 } ) await journal.close() @@ -306,6 +309,16 @@ describe('already-wedged profiles become usable on load', () => { 'a quit that left the owner for a probe to prove gone', wedgedRecord({ claimStatus: 'live', handoffStage: null, ownerProcess: DEAD_OWNER }), null, + { state: 'interrupted', completedAt: SEEDED_TURN_STARTED_AT } + ], + [ + 'a quit that left an owner on a host this one cannot probe', + wedgedRecord({ + claimStatus: 'live', + handoffStage: null, + ownerProcess: { ...DEAD_OWNER, hostId: 'remote-host' } + }), + null, { state: 'unverifiable' } ] ] as const)( @@ -314,8 +327,18 @@ describe('already-wedged profiles become usable on load', () => { await seedStore({ ...seeded, lease: { ...seeded.lease, deathEvidence } }) await seedRunningTurn() const published: AgentSessionStatusSummary[] = [] + const remote = seeded.lease.ownerProcess?.hostId === 'remote-host' openHost({ - statusSink: { publish: (summary) => published.push(summary), forget: () => {} } + statusSink: { publish: (summary) => published.push(summary), forget: () => {} }, + // Loss of contact is never proof of death: a remote owner only ever probes indeterminate. + ...(remote + ? { + probeOwner: async () => ({ + outcome: 'indeterminate' as const, + reason: 'owner runs on remote-host, which this host cannot probe' + }) + } + : {}) }) await host.restoreReadableSessions() @@ -328,9 +351,25 @@ describe('already-wedged profiles become usable on load', () => { ...verdict }) expect(activeStructuredAgentSessionTurnId(restoredJournal().snapshot().items)).toBe(null) + // The row never carries the proof's detail, which is Orca's log text. + const statusRows = restoredJournal() + .snapshot() + .items.flatMap((item) => (item.body.kind === 'status' ? [item.body.text] : [])) + expect(statusRows).toEqual( + remote + ? [] + : [ + 'Codex stopped while this response was in progress. You can continue in this conversation.' + ] + ) // What the sidebar reads: every status this restart published says the chat is not working. expect(published.filter((summary) => summary.sessionId === SESSION)).not.toEqual([]) expect(published.map((summary) => summary.status)).not.toContain('working') + // A crash is not something the user did: no outcome is claimed, so no reader files it as a + // cancellation the user already knows about. + expect(published.map((summary) => summary.turnOutcome)).toEqual( + published.map(() => undefined) + ) } ) @@ -350,11 +389,12 @@ describe('already-wedged profiles become usable on load', () => { expect(acquire).toHaveBeenCalledOnce() expect(activeStructuredAgentSessionTurnId(restoredJournal().snapshot().items)).toBe(null) - // A pid probe proved the owner gone; nobody saw it exit, so the turn has no end. + // A pid probe proved the owner gone, so the turn was cut short when it was last proven alive. expect(turnLifecycle('turn-1')).toEqual({ turnId: 'turn-1', - state: 'unverifiable', + state: 'interrupted', startedAt: NOW - 5_000, + completedAt: SEEDED_TURN_STARTED_AT, recovered: true }) expect(store.getRecord(SESSION)?.lease).toMatchObject({ @@ -443,11 +483,12 @@ describe('already-wedged profiles become usable on load', () => { expect(acquire).toHaveBeenCalledOnce() expect(store.getRecord(SESSION)?.lease).toMatchObject({ claimStatus: 'live' }) - // A pid probe proved the owner gone; nobody saw it exit, so the turn has no end. + // A pid probe proved the owner gone, so the turn was cut short when it was last proven alive. expect(turnLifecycle('turn-1')).toEqual({ turnId: 'turn-1', - state: 'unverifiable', + state: 'interrupted', startedAt: NOW - 5_000, + completedAt: SEEDED_TURN_STARTED_AT, recovered: true }) } diff --git a/src/main/opencode/hook-plugin-dispose-host.test.ts b/src/main/opencode/hook-plugin-dispose-host.test.ts new file mode 100644 index 00000000000..179714d8c83 --- /dev/null +++ b/src/main/opencode/hook-plugin-dispose-host.test.ts @@ -0,0 +1,165 @@ +/** + * Executes the generated plugin through each OpenCode host entry point, because + * what disposal means differs by host: OpenCode 1 disposes only on instance + * teardown (which cancels every run), OpenCode 2 also on a plugin hot reload. + */ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { getPathMock } = vi.hoisted(() => ({ + getPathMock: vi.fn<(name: string) => string>() +})) + +vi.mock('electron', () => ({ + app: { getPath: getPathMock } +})) + +import { _internals } from './hook-service' + +type PluginHooks = { + event: (input: { event: unknown }) => Promise + dispose?: () => Promise +} +type HostEvent = { type: string; data: Record } +type PluginModule = { + default?: { + server?: (ctx: unknown) => Promise + setup?: (ctx: unknown) => Promise<() => Promise> + } +} + +const ENV_KEYS = [ + 'ORCA_PANE_KEY', + 'ORCA_OPENCODE_AGENT', + 'ORCA_AGENT_HOOK_ENDPOINT', + 'ORCA_AGENT_HOOK_PORT', + 'ORCA_AGENT_HOOK_TOKEN' +] as const + +// A live OpenCode 2 event bus: stays open until the subscriber aborts. +function createEventBus(): { + push: (event: HostEvent) => void + subscribe: (input: { signal: AbortSignal }) => AsyncGenerator +} { + const queue: HostEvent[] = [] + let wake: (() => void) | null = null + return { + push(event) { + queue.push(event) + wake?.() + }, + async *subscribe({ signal }) { + while (!signal.aborted) { + const next = queue.shift() + if (next) { + yield next + continue + } + await new Promise((resolve) => { + wake = resolve + signal.addEventListener('abort', () => resolve(), { once: true }) + }) + wake = null + } + } + } +} + +describe.each(['opencode', 'opencode2'] as const)('%s plugin disposal by host', (agent) => { + let tempDir: string + let savedEnv: Record + let names: string[] + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'orca-opencode-dispose-host-')) + savedEnv = {} + for (const key of ENV_KEYS) { + savedEnv[key] = process.env[key] + } + process.env.ORCA_PANE_KEY = 'tab-1:leaf-1' + process.env.ORCA_OPENCODE_AGENT = agent + delete process.env.ORCA_AGENT_HOOK_ENDPOINT + process.env.ORCA_AGENT_HOOK_PORT = '59999' + process.env.ORCA_AGENT_HOOK_TOKEN = 'test-token' + names = [] + vi.stubGlobal( + 'fetch', + vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { + names.push(String(JSON.parse(String(init?.body)).payload?.hook_event_name)) + return new Response('{}', { status: 200 }) + }) + ) + }) + + afterEach(() => { + vi.unstubAllGlobals() + for (const key of ENV_KEYS) { + if (savedEnv[key] === undefined) { + delete process.env[key] + } else { + process.env[key] = savedEnv[key] + } + } + rmSync(tempDir, { recursive: true, force: true }) + }) + + async function loadPluginModule(): Promise { + const pluginPath = join(tempDir, `orca-${agent}-status.mjs`) + writeFileSync( + pluginPath, + agent === 'opencode2' + ? _internals.getOpenCode2PluginSource() + : _internals.getOpenCodePluginSource() + ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the generated plugin module exports this shape. + return (await import(pathToFileURL(pluginPath).href)) as PluginModule + } + + function setupContext(bus: ReturnType): unknown { + return { + session: { + get: async ({ sessionID }: { sessionID: string }) => ({ id: sessionID }), + hook: async () => ({ dispose: async () => {} }) + }, + event: { subscribe: bus.subscribe } + } + } + + it('publishes a final Idle when the OpenCode 1 instance tears down mid-turn', async () => { + const module = await loadPluginModule() + const hooks = await module.default?.server?.({ + client: { session: { get: async () => ({ data: { id: 'ses_root' } }) } } + }) + await hooks?.event({ + event: { + type: 'session.status', + properties: { sessionID: 'ses_root', status: { type: 'busy' } } + } + }) + await hooks?.dispose?.() + + expect(names).toEqual(['SessionBusy', 'SessionIdle']) + }) + + it('keeps the pane Working when OpenCode 2 reloads the plugin mid-turn', async () => { + const module = await loadPluginModule() + const firstBus = createEventBus() + const firstCleanup = await module.default?.setup?.(setupContext(firstBus)) + firstBus.push({ type: 'session.execution.started', data: { sessionID: 'ses_root' } }) + await vi.waitFor(() => expect(names).toEqual(['SessionBusy'])) + + await firstCleanup?.() + expect(names).toEqual(['SessionBusy']) + + // The turn kept running; its end reaches the reloaded plugin. + const secondBus = createEventBus() + const secondCleanup = await module.default?.setup?.(setupContext(secondBus)) + secondBus.push({ type: 'session.execution.succeeded', data: { sessionID: 'ses_root' } }) + await vi.waitFor(() => expect(names).toEqual(['SessionBusy', 'SessionIdle'])) + await secondCleanup?.() + expect(names).toEqual(['SessionBusy', 'SessionIdle']) + }) +}) diff --git a/src/main/opencode/hook-service.test.ts b/src/main/opencode/hook-service.test.ts index 66fb3c86a47..e99550c59ea 100644 --- a/src/main/opencode/hook-service.test.ts +++ b/src/main/opencode/hook-service.test.ts @@ -97,11 +97,11 @@ describe('OpenCode hook plugin source', () => { const digest = (source: string): string => createHash('sha256').update(source).digest('hex') expect(digest(getOpenCodePluginSource())).toBe( - 'c81893814f08bfb2f9b5c133c5355b53d485982402d89e92ff15fc4e7d543079' + '655d603f31c3e4462b4c600861833cbdba9dd541f693e11a349e7a57b8c059d7' ) expect( digest(getOpenCodeFamilyPluginSource('/hook/mimo-code', { emitSessionStart: false })) - ).toBe('2267e2ab6e854e71c9bae12afed97e464f93b2b14f3e25dca133ca6666c98752') + ).toBe('d36d869823cc3dd506565e0512beb278fe4fd6533bd12be30bd98db140ca9f3e') }) it('filters child sessions via parentID lookup before forwarding events', () => { @@ -294,6 +294,51 @@ describe('OpenCodeHookService buildPtyEnv / clearPty round-trip', () => { expect(pluginSource).toContain('messageID: part.messageID') }) + // Why: OpenCode 2 reloads a plugin whose file mtime changed, which restarted status mid-turn. + it('leaves a current installed plugin untouched and replaces a stale one', () => { + const service = new OpenCodeHookService() + service.buildPtyEnv(daemonSessionId) + const pluginPath = join(resolveOpenCodeConfigDirectory(), 'plugins', 'orca-opencode-status.js') + const past = new Date('2020-01-01T00:00:00Z') + utimesSync(pluginPath, past, past) + + service.buildPtyEnv(daemonSessionId) + expect(statSync(pluginPath).mtimeMs).toBe(past.getTime()) + + writeFileSync(pluginPath, 'stale plugin') + service.buildPtyEnv(daemonSessionId) + expect(readFileSync(pluginPath, 'utf8')).toBe(_internals.getOpenCodePluginSource()) + }) + + // Why: OpenCode 2 loads through a file-level symlink (dotfile managers) and stats its target. + it.skipIf(process.platform === 'win32')( + 'compares a symlinked plugin by its target and writes through only when stale', + () => { + const service = new OpenCodeHookService() + const pluginPath = join( + resolveOpenCodeConfigDirectory(), + 'plugins', + 'orca-opencode-status.js' + ) + const targetPath = join(userDataDir, 'dotfiles-orca-opencode-status.js') + writeFileSync(targetPath, _internals.getOpenCodePluginSource()) + rmSync(pluginPath, { force: true }) + symlinkSync(targetPath, pluginPath) + const past = new Date('2020-01-01T00:00:00Z') + utimesSync(targetPath, past, past) + + service.buildPtyEnv(daemonSessionId) + expect(statSync(targetPath).mtimeMs).toBe(past.getTime()) + + writeFileSync(targetPath, 'stale plugin') + service.buildPtyEnv(daemonSessionId) + expect(lstatSync(pluginPath).isSymbolicLink()).toBe(true) + expect(readFileSync(targetPath, 'utf8')).toBe(_internals.getOpenCodePluginSource()) + rmSync(pluginPath, { force: true }) + rmSync(targetPath, { force: true }) + } + ) + // Why: #22234 — OpenCode 2 installs under the plain `opencode` name, and its loader // rejects a default export that only has server(). Asserting the emitted *source* is // not enough; the installed file is what the v2 server validates, so load it. @@ -700,6 +745,19 @@ describe('OpenCodeHookService overlay mode (user OPENCODE_CONFIG_DIR set)', () = expectUserConfigIntact() }) + it('leaves a current overlay plugin file in place across spawns', () => { + const service = new OpenCodeHookService() + const overlayDir = service.buildPtyEnv(ptyId, userConfigDir).OPENCODE_CONFIG_DIR! + const pluginPath = join(overlayDir, 'plugins', 'orca-opencode-status.js') + const past = new Date('2020-01-01T00:00:00Z') + utimesSync(pluginPath, past, past) + + service.buildPtyEnv(ptyId, userConfigDir) + + expect(statSync(pluginPath).mtimeMs).toBe(past.getTime()) + expect(readFileSync(pluginPath, 'utf8')).toBe(_internals.getOpenCodePluginSource()) + }) + it('reconciles stale mirrored entries while preserving OpenCode runtime files', () => { const service = new OpenCodeHookService() const firstEnv = service.buildPtyEnv(ptyId, userConfigDir) diff --git a/src/main/opencode/hook-service.ts b/src/main/opencode/hook-service.ts index e7ec0cc8b2f..74f24577c6e 100644 --- a/src/main/opencode/hook-service.ts +++ b/src/main/opencode/hook-service.ts @@ -28,6 +28,10 @@ import { OPENCODE2_LEGACY_HOOKS_DIR, OPENCODE_LEGACY_HOOKS_DIR } from './legacy-shared-config-dir' +import { + isInstalledOpenCodePluginCurrent, + isOverlayOpenCodePluginCurrent +} from '../../shared/opencode-installed-plugin' const ORCA_OPENCODE_PLUGIN_FILE = 'orca-opencode-status.js' const OPENCODE_OVERLAY_DIR = 'opencode-config-overlays' @@ -278,18 +282,25 @@ export class OpenCodeHookService { const pluginsDir = join(overlayDir, 'plugins') mkdirSync(pluginsDir, { recursive: true }) const pluginPath = join(pluginsDir, this.pluginFileName) - try { - unlinkSync(pluginPath) - } catch { - // File may not exist on a fresh overlay; a real failure surfaces on writeFileSync below. + const source = this.pluginSource() + if (!isOverlayOpenCodePluginCurrent(pluginPath, source)) { + try { + unlinkSync(pluginPath) + } catch { + // File may not exist on a fresh overlay; a real failure surfaces on writeFileSync below. + } + writeFileSync(pluginPath, source) } - writeFileSync(pluginPath, this.pluginSource()) } private writePluginToConfigDir(configDir: string): void { const pluginsDir = join(configDir, 'plugins') mkdirSync(pluginsDir, { recursive: true }) - writeFileSync(join(pluginsDir, this.pluginFileName), this.pluginSource()) + const pluginPath = join(pluginsDir, this.pluginFileName) + const source = this.pluginSource() + if (!isInstalledOpenCodePluginCurrent(pluginPath, source)) { + writeFileSync(pluginPath, source) + } } } diff --git a/src/main/opencode/status-plugin-factory-source.ts b/src/main/opencode/status-plugin-factory-source.ts index 8348cc968eb..218d15c3e41 100644 --- a/src/main/opencode/status-plugin-factory-source.ts +++ b/src/main/opencode/status-plugin-factory-source.ts @@ -23,6 +23,7 @@ export function getStatusPluginFactorySource(options: { 'export const OrcaOpenCodeStatusPlugin = async (_ctx) => {', ` if (process.env.ORCA_OPENCODE_AGENT && process.env.ORCA_OPENCODE_AGENT !== '${expectedAgent}') return {};`, ' const client = _ctx?.client;', + ' const sessionsOutliveDispose = _ctx?.sessionsOutliveDispose === true;', ' const factoryID = ++nextFactoryID;', ' activeFactoryIDs.add(factoryID);', ' let disposed = false;', @@ -242,7 +243,14 @@ export function getStatusPluginFactorySource(options: { ' pendingAssistantPart = null;', ' }', ' const ownsDeliveredMessagePart = deliveredMessagePartFactoryID === factoryID;', - ' if (desiredFactoryID === factoryID || ownsDeliveredMessagePart) {', + ' // Why: OpenCode 1 disposes only on instance teardown, which cancels every run, so a final', + ' // Idle is true. OpenCode 2 also disposes on a hot reload mid-turn, so it publishes nothing.', + ' if (sessionsOutliveDispose) {', + ' if (desiredFactoryID === factoryID) {', + ' clearStatusRetry();', + ' statusRevision += 1;', + ' }', + ' } else if (desiredFactoryID === factoryID || ownsDeliveredMessagePart) {', ' clearStatusRetry();', ' statusRevision += 1;', ' // A MessagePart may have changed the listener to Working after the', @@ -250,17 +258,13 @@ export function getStatusPluginFactorySource(options: { ' statusDeliveryDirty = ownsDeliveredMessagePart;', ' busyRecoveryUsed = false;', ' busyRecoveryEndpointKey = "";', - ' const fallbackFactoryID = Array.from(activeFactoryIDs).find(', - ' (id) => id !== factoryID', - ' );', + ' const fallbackFactoryID = Array.from(activeFactoryIDs).find((id) => id !== factoryID);', ' if (fallbackFactoryID !== undefined) {', ' await publishAggregateStatus(', ' fallbackFactoryID,', ' desiredStatusProperties?.sessionID', ' );', ' } else {', - ' // Why: Instance disposal can happen while the PTY stays alive;', - ' // publish a final idle so Orca does not retain a dead owner.', ' if (!deliveredStatusKey.startsWith("idle:") || ownsDeliveredMessagePart) {', ' await setStatus(', ' "idle",', diff --git a/src/main/opencode2/status-plugin-setup-source.ts b/src/main/opencode2/status-plugin-setup-source.ts index c68c15cbda2..7b825056ecd 100644 --- a/src/main/opencode2/status-plugin-setup-source.ts +++ b/src/main/opencode2/status-plugin-setup-source.ts @@ -24,7 +24,8 @@ async function setupOpenCode2Status(ctx) { // Without it, resolveRootSessionID returns null for every session and a // subagent's work publishes as if it were the root's. const client = { session: { get: async (input, options) => { const result = await ctx.session.get(input, options); return result && typeof result.id === "string" ? { data: result } : result; } } }; - hooks = await OrcaOpenCodeStatusPlugin({ client }); + // Why: this host disposes plugins on a hot reload while turns keep running. + hooks = await OrcaOpenCodeStatusPlugin({ client, sessionsOutliveDispose: true }); if (!hooks || typeof hooks.event !== "function") return noop; const promptRegistration = await ctx.session.hook("prompt", async (properties) => { await hooks.event({ event: { type: "session.next.prompt.admitted", properties } }); diff --git a/src/main/persistence-host-admitted-terminal-membership.test.ts b/src/main/persistence-host-admitted-terminal-membership.test.ts index 3470215a515..83a6898c667 100644 --- a/src/main/persistence-host-admitted-terminal-membership.test.ts +++ b/src/main/persistence-host-admitted-terminal-membership.test.ts @@ -136,7 +136,7 @@ describe('host-admitted terminal membership survives a stale renderer replay', ( }) // Closing must still work afterwards. Closes are host-driven: the retirement is - // computed from the store's own session (see persistTerminalSurfaceRetirements), + // computed from the store's own session (see stageTerminalSurfaceRetirements), // which is what outranks the fence this create just raised. it('still lets the authoritative retirement path close the host-admitted tab', async () => { const store = await createStore() diff --git a/src/main/persistence/loading-store/profile-state-delayed-authority-fixture.ts b/src/main/persistence/loading-store/profile-state-delayed-authority-fixture.ts index cebd91b0538..26709b33e3f 100644 --- a/src/main/persistence/loading-store/profile-state-delayed-authority-fixture.ts +++ b/src/main/persistence/loading-store/profile-state-delayed-authority-fixture.ts @@ -29,12 +29,18 @@ export class DelayedAuthority implements AsyncProfileStateAuthority { | { started: ReturnType>; finish: ReturnType> } | undefined readonly captures: ProfileStateDomainReplacement[][] = [] + private failNext = false readonly close = vi.fn(async () => { this.inner.close() }) constructor(readonly inner: ProfileStateSqliteAuthority) {} + /** Fail the next profile write the way a full disk would, after any paused gate opens. */ + failNextWrite() { + this.failNext = true + } + pause() { const gate = { started: deferred(), finish: deferred() } this.next = gate @@ -51,17 +57,17 @@ export class DelayedAuthority implements AsyncProfileStateAuthority { } async writeSerializedState(payload: Buffer) { const captured = Buffer.from(payload) - await this.dispatch(() => this.inner.writeSerializedState(captured)) + await this.dispatch(() => this.inner.writeSerializedState(captured), true) } async writeSerializedDomains(replacements: readonly ProfileStateDomainReplacement[]) { const captured = structuredClone(replacements) this.captures.push([...captured]) - await this.dispatch(() => this.inner.writeSerializedDomains(captured)) + await this.dispatch(() => this.inner.writeSerializedDomains(captured), true) } async writeCompleteSerializedDomains(replacements: readonly ProfileStateDomainReplacement[]) { const captured = structuredClone(replacements) this.captures.push([...captured]) - await this.dispatch(() => this.inner.writeCompleteSerializedDomains(captured)) + await this.dispatch(() => this.inner.writeCompleteSerializedDomains(captured), true) } async writeSerializedAutomationRuns( replacements: readonly ProfileStateDomainReplacement[], @@ -69,7 +75,10 @@ export class DelayedAuthority implements AsyncProfileStateAuthority { ) { const captured = structuredClone(replacements) const capturedRuns = structuredClone(runs) - await this.dispatch(() => this.inner.writeSerializedAutomationRuns(captured, capturedRuns)) + await this.dispatch( + () => this.inner.writeSerializedAutomationRuns(captured, capturedRuns), + true + ) } async writeJsonExport(path: string) { return this.inner.writeJsonExport(path) @@ -83,11 +92,15 @@ export class DelayedAuthority implements AsyncProfileStateAuthority { async quarantineDatabase(root?: string, reason?: string) { return this.inner.quarantineDatabase(root, reason) } - private async dispatch(operation: () => void) { + private async dispatch(operation: () => void, write = false) { const gate = this.next this.next = undefined gate?.started.resolve() await gate?.finish.promise + if (write && this.failNext) { + this.failNext = false + throw new Error('profile_state_write_failed') + } operation() } } diff --git a/src/main/persistence/loading-store/pty-retirement-publication-during-read.test.ts b/src/main/persistence/loading-store/pty-retirement-publication-during-read.test.ts index dd6754c1d73..34b6e742804 100644 --- a/src/main/persistence/loading-store/pty-retirement-publication-during-read.test.ts +++ b/src/main/persistence/loading-store/pty-retirement-publication-during-read.test.ts @@ -29,9 +29,6 @@ class RetirementRuntime extends OrcaRuntimeService { generations(): number { return this.ptyLifecycleGenerationById.size } - retirements(): number { - return this.pendingPtySurfaceRetirementsByPtyId.size - } async closeTab(): Promise { const snapshot = this.snapshot() const tab = snapshot?.tabs[0] @@ -96,10 +93,8 @@ it.each(['read', 'replacement', 'legacy-replacement'] as const)( gate.finish.resolve() await exiting expect(readState().workspaceSession.terminalLayoutsByTabId[binding.tabId]).toBeUndefined() - expect(runtime.snapshot()?.tabs).toHaveLength(action === 'read' ? 0 : 1) - if (action === 'read') { - expect(runtime.retirements()).toBe(0) - } + // Why every action: the exit retired the leaf in memory before any of them could run. + expect(runtime.snapshot()?.tabs).toHaveLength(0) } ) diff --git a/src/main/persistence/loading-store/pty-spawn-replacement-durability.test.ts b/src/main/persistence/loading-store/pty-spawn-replacement-durability.test.ts index 719a3bbe924..202ad5e6a2d 100644 --- a/src/main/persistence/loading-store/pty-spawn-replacement-durability.test.ts +++ b/src/main/persistence/loading-store/pty-spawn-replacement-durability.test.ts @@ -1,3 +1,4 @@ +import { isTerminalSessionStorageCapacityFailure } from '../../../shared/terminal-session-state-save-failure' import { afterEach, expect, it, vi } from 'vitest' import { deferred, fixture } from './profile-state-delayed-authority-fixture' import { OrcaRuntimeService } from '../../runtime/orca-runtime' @@ -36,11 +37,17 @@ afterEach(() => { it.each( ['ipc', 'runtime'].flatMap((controller) => - ['save', 'shutdown'].map((replacementDuring) => ({ controller, replacementDuring })) + ['save', 'shutdown'].flatMap((replacementDuring) => + ['profile-state-writer-exit', 'ENOSPC'].map((code) => ({ + controller, + replacementDuring, + code + })) + ) ) )( - 'preserves a successor during $replacementDuring when the predecessor $controller save fails', - async ({ controller, replacementDuring }) => { + 'preserves a successor during $replacementDuring when the predecessor $controller save fails with $code', + async ({ controller, replacementDuring, code }) => { const { store, authority } = await fixture() const runtime = new OrcaRuntimeService(store) runtime.onPtySpawned(binding.ptyId, binding.incarnationId) @@ -70,10 +77,18 @@ it.each( commit = () => commitRuntimePtySpawn(ctx) } const gate = authority.pause() - const pending = expect(commit()).rejects.toThrow('ORCA_TERMINAL_SESSION_STATE_SAVE_FAILED') + const pending = commit().catch((error: unknown) => { + expect(error).toBeInstanceOf(Error) + if (!(error instanceof Error)) { + throw error + } + expect(error.message).toContain('ORCA_TERMINAL_SESSION_STATE_SAVE_FAILED') + expect(isTerminalSessionStorageCapacityFailure(error.message)).toBe(code === 'ENOSPC') + return 'rejected' + }) await gate.started.promise if (replacementDuring === 'shutdown') { - gate.finish.reject(new Error('disk full')) + gate.finish.reject(Object.assign(new Error('worker exited'), { code })) await shutdownStarted.promise } await runtime.onPtyExit(binding.ptyId, 0, binding.incarnationId, { providerExitObserved: true }) @@ -81,10 +96,10 @@ it.each( ptyIncarnationById.set(binding.ptyId, replacementIncarnation) ptyOwnership.set(binding.ptyId, 'successor-host') if (replacementDuring === 'save') { - gate.finish.reject(new Error('disk full')) + gate.finish.reject(Object.assign(new Error('worker exited'), { code })) } shutdownFinished.resolve() - await pending + expect(await pending).toBe('rejected') if (replacementDuring === 'save') { expect(shutdown).not.toHaveBeenCalled() } else { @@ -144,3 +159,14 @@ it.each(['ipc', 'runtime'])( }) } ) + +it('does not label an IPC binding ownership refusal as a storage failure', async () => { + const { store } = await fixture() + const runtime = new OrcaRuntimeService(store) + const deps = createPtySpawnCommitDependencies(runtime, store) + const ctx = createPtyIpcSpawnState(deps, { ...binding, cols: 80, rows: 24 }) + ctx.result = { id: binding.ptyId, incarnationId: binding.incarnationId } + ctx.validatedLeafId = binding.leafId + vi.spyOn(store, 'persistPtyBinding').mockResolvedValue(false) + await expect(commitPtyIpcSpawn(ctx)).rejects.toThrow('terminal_pane_owner_changed') +}) diff --git a/src/main/persistence/profile-state/profile-state-writer-capacity-errors.test.ts b/src/main/persistence/profile-state/profile-state-writer-capacity-errors.test.ts new file mode 100644 index 00000000000..f537af8833c --- /dev/null +++ b/src/main/persistence/profile-state/profile-state-writer-capacity-errors.test.ts @@ -0,0 +1,47 @@ +import Database from '../../sqlite/sync-database' +import { expect, it } from 'vitest' +import { + decodeProfileStateWriterError, + encodeProfileStateWriterError +} from './profile-state-writer-errors' +import { + createTerminalSessionStateSaveFailureMessage, + isTerminalSessionStorageCapacityFailure +} from '../../../shared/terminal-session-state-save-failure' + +it.each(['ENOSPC', 'EDQUOT', 'SQLITE_FULL', 'EACCES', 'SQLITE_BUSY'])( + 'preserves capacity evidence across the writer boundary for %s', + (code) => { + const error = new Error('private path must not cross the worker boundary', { + cause: Object.assign(new Error('private details'), { code }) + }) + const encoded = encodeProfileStateWriterError(error) + expect(encoded.message).toBe('Profile state persistence failed') + const decoded = decodeProfileStateWriterError(JSON.parse(JSON.stringify(encoded))) + const capacity = ['ENOSPC', 'EDQUOT', 'SQLITE_FULL'].includes(code) + expect( + isTerminalSessionStorageCapacityFailure(createTerminalSessionStateSaveFailureMessage(decoded)) + ).toBe(capacity) + } +) + +it('classifies a real SQLite capacity failure after worker serialization', () => { + const db = new Database(':memory:') + try { + db.exec('PRAGMA page_size=512; CREATE TABLE writes(data BLOB); PRAGMA max_page_count=2') + let failure: unknown + try { + db.exec('INSERT INTO writes VALUES (zeroblob(4096))') + } catch (error) { + failure = error + } + expect(failure).toBeDefined() + const decoded = decodeProfileStateWriterError(encodeProfileStateWriterError(failure)) + expect( + isTerminalSessionStorageCapacityFailure(createTerminalSessionStateSaveFailureMessage(decoded)) + ).toBe(true) + db.exec("INSERT INTO writes VALUES ('small')") + } finally { + db.close() + } +}) diff --git a/src/main/persistence/profile-state/profile-state-writer-errors.ts b/src/main/persistence/profile-state/profile-state-writer-errors.ts index 28237862ce2..afb30d520c8 100644 --- a/src/main/persistence/profile-state/profile-state-writer-errors.ts +++ b/src/main/persistence/profile-state/profile-state-writer-errors.ts @@ -1,3 +1,4 @@ +import { storageCapacityErrorCode } from '../../../shared/storage-capacity-error' import { ProfileStateDocumentCorruptionError, ProfileStateRevisionConflictError @@ -64,7 +65,7 @@ export function encodeProfileStateWriterError( return { code: error.code, message: error.message, outcome } } return { - code: 'profile-state-write-failed', + code: storageCapacityErrorCode(error) ?? 'profile-state-write-failed', message: 'Profile state persistence failed', outcome } diff --git a/src/main/remote-agent-trust-presets.test.ts b/src/main/remote-agent-trust-presets.test.ts index 8b552e55277..4b0692bb142 100644 --- a/src/main/remote-agent-trust-presets.test.ts +++ b/src/main/remote-agent-trust-presets.test.ts @@ -159,6 +159,61 @@ describe('markRemoteAgentWorkspaceTrusted', () => { expect(fsProvider.writeFile).not.toHaveBeenCalled() }) + + it.each(['codex', 'copilot'] as const)( + 'leaves the remote %s config untouched when the read fails', + async (preset) => { + const fsProvider = makeFsProvider({ + readFile: vi.fn(async () => { + throw new Error('file stream stalled') + }) + }) + mocks.getSshFilesystemProvider.mockReturnValue(fsProvider) + + await expect( + markRemoteAgentWorkspaceTrusted({ preset, connectionId: 'ssh-1', workspacePath: '/repo' }) + ).rejects.toThrow() + + expect(fsProvider.writeFile).not.toHaveBeenCalled() + } + ) + + it.each(['codex', 'copilot'] as const)( + 'leaves the remote %s config untouched when the read comes back binary', + async (preset) => { + const fsProvider = makeFsProvider({ + readFile: vi.fn(async () => ({ content: 'AAEC', isBinary: true })) + }) + mocks.getSshFilesystemProvider.mockReturnValue(fsProvider) + + await expect( + markRemoteAgentWorkspaceTrusted({ preset, connectionId: 'ssh-1', workspacePath: '/repo' }) + ).rejects.toThrow() + + expect(fsProvider.writeFile).not.toHaveBeenCalled() + } + ) + + it('creates the remote Codex config when the relay reports it missing', async () => { + // The relay rebuilds remote errors without Node's .code; only the message survives. + const fsProvider = makeFsProvider({ + readFile: vi.fn(async () => { + throw new Error("ENOENT: no such file or directory, stat '/home/u/.codex/config.toml'") + }) + }) + mocks.getSshFilesystemProvider.mockReturnValue(fsProvider) + + await markRemoteAgentWorkspaceTrusted({ + preset: 'codex', + connectionId: 'ssh-1', + workspacePath: '/repo' + }) + + expect(fsProvider.writeFile).toHaveBeenCalledWith( + '/home/u/.codex/config.toml', + '[projects."/real/repo"]\ntrust_level = "trusted"\n' + ) + }) }) describe('remote Qoder trust', () => { diff --git a/src/main/remote-agent-trust-presets.ts b/src/main/remote-agent-trust-presets.ts index 9ca8d665e06..021f18683ca 100644 --- a/src/main/remote-agent-trust-presets.ts +++ b/src/main/remote-agent-trust-presets.ts @@ -3,7 +3,8 @@ import type { AgentTrustPreset } from './agent-trust-presets' import { upsertProjectTrustLevelInContent } from './codex/config-toml-trust' import { getActiveMultiplexer } from './ssh/ssh-target-registry' import { getSshFilesystemProvider } from './providers/ssh-filesystem-dispatch' -import type { IFilesystemProvider } from './providers/types' +import type { FileReadResult, IFilesystemProvider } from './providers/types' +import { isENOENT } from './ipc/filesystem-path-containment' import { isWindowsAbsolutePathLike, normalizeRuntimePathSeparators @@ -76,12 +77,20 @@ async function readRemoteTextFile( fsProvider: IFilesystemProvider, filePath: string ): Promise { + let result: FileReadResult try { - const result = await fsProvider.readFile(filePath) - return result.isBinary ? '' : result.content - } catch { - return '' + result = await fsProvider.readFile(filePath) + } catch (error) { + // Why: only definitive absence may seed empty; a failed read would otherwise overwrite the user's config. + if (isENOENT(error)) { + return '' + } + throw error } + if (result.isBinary) { + throw new Error(`Refusing to rewrite non-text remote config: ${filePath}`) + } + return result.content } async function markRemoteCodexProjectTrusted( diff --git a/src/main/runtime/__fixtures__/codex-0-150-1-turn.meta.json b/src/main/runtime/__fixtures__/codex-0-150-1-turn.meta.json new file mode 100644 index 00000000000..21b39d7625b --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-150-1-turn.meta.json @@ -0,0 +1,16 @@ +{ + "capturedAt": "2026-09-29T01:35:32.695Z", + "platform": "darwin", + "command": [ + "codex", + "-c", + "check_for_update_on_startup=false", + "--dangerously-bypass-approvals-and-sandbox", + "-m", + "gpt-5.6-sol" + ], + "cols": 120, + "rows": 40, + "note": "codex-cli 0.150.1: launch, one short turn, settled idle after it. STA-8834.", + "exitCode": 0 +} diff --git a/src/main/runtime/__fixtures__/codex-0-150-1-turn.txt b/src/main/runtime/__fixtures__/codex-0-150-1-turn.txt new file mode 100644 index 00000000000..4d4c5dacccf --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-150-1-turn.txt @@ -0,0 +1,18 @@ +[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h╭───────────────────────────────────────╮│ >_ OpenAI Codex (v0.150.1) ││ ││ model: loading /model to change ││ directory: loading │╰───────────────────────────────────────╯  › Ask Codex to do anything  ? for shortcuts[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h╭──────────────────────────────────────────────────╮│ >_ OpenAI Codex (v0.150.1) ││ ││ model: loading /model to change ││ directory: ~/orca-lanes/sta8834/corpus/scratch ││ permissions: YOLO mode │╰──────────────────────────────────────────────────╯  › Ask Codex to do anything  ? for shortcuts[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;scratch[?2026h[0 q[?25h[?2026l[?2026hgpt-5.6-sol default · ~/orca-lanes/sta8834/corpus/scratch[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠋ scratch]0;⠙ scratch[?2026hMMMMMMMMM +╭────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.150.1) │ +│ │ +│ model: gpt-5.6-sol medium /model to change │ +│ directory: ~/orca-lanes/sta8834/corpus/scratch │ +│ permissions: YOLO mode │ +╰────────────────────────────────────────────────────╯ + + Tip: Try the Desktop app. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true • Booting MCP server: node_repl (0s • esc to interrupt)  › Ask Codex to do anything  gpt-5.6-sol medium · ~/orca-lanes/sta8834/corpus/scratch[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;scratch[?2026h  › Ask Codex to do anything  gpt-5.6-sol medium · ~/orca-lanes/sta8834/corpus/scratch[0 q[?25h[?2026l[?2026hMM + +• You have 3 usage limit resets available. Run /usage to use one.[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hReply with the single wordOK.[0 q[?25h[?2026l[?2026hMMMM + + +› Reply with the single word OK. +Ask Codex to do anything[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠋ scratch[?2026h • Working (0s • esc to interrupt)  › Ask Codex to do anything  gpt-5.6-sol medium · ~/orca-lanes/sta8834/corpus/scratch[0 q[?25h[?2026l]0;⠙ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠹ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠸ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠼ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠴ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hW[0 q[?25h[?2026l]0;⠦ scratch[?2026h[0 q[?25h[?2026l[?2026hWo[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠧ scratch[?2026h•Wor[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠇ scratch[?2026h•Work[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h•Worki[0 q[?25h[?2026l]0;⠏ scratch[?2026h[0 q[?25h[?2026l[?2026hWorkin[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠋ scratch[?2026hWorking[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h•Working1[0 q[?25h[?2026l]0;⠙ scratch[?2026h[0 q[?25h[?2026l[?2026hWorking[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠹ scratch[?2026h[0 q[?25h[?2026l[?2026hWorking[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠸ scratch[?2026horking[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l[?2026hrking[0 q[?25h[?2026l]0;⠼ scratch[?2026h[0 q[?25h[?2026l[?2026h•king[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠴ scratch[?2026hing[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠦ scratch[?2026hng[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠧ scratch[?2026hg[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠇ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠏ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠋ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠙ scratch[?2026h2[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠹ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠸ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠼ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠴ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hW[0 q[?25h[?2026l]0;⠦ scratch[?2026h[0 q[?25h[?2026l[?2026hWo[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠧ scratch[?2026h•Wor[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠇ scratch[?2026h•Work[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h•Worki[0 q[?25h[?2026l]0;⠏ scratch[?2026h[0 q[?25h[?2026l[?2026hWorkin[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠋ scratch[?2026hWorking[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h•Working3[0 q[?25h[?2026l]0;⠙ scratch[?2026h[0 q[?25h[?2026l[?2026hWorking[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠹ scratch[?2026h[0 q[?25h[?2026l[?2026hWorking[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠸ scratch[?2026horking[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l[?2026hrking[0 q[?25h[?2026l]0;⠼ scratch[?2026h[0 q[?25h[?2026l[?2026h•king[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠴ scratch[?2026hing[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠦ scratch[?2026hng[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hg[0 q[?25h[?2026l]0;⠧ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠇ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠏ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠋ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠙ scratch[?2026h4[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠹ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠸ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠼ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠴ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hW[0 q[?25h[?2026l]0;⠦ scratch[?2026h[0 q[?25h[?2026l[?2026hWo[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠧ scratch[?2026h•Wor[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠇ scratch[?2026h•Work[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hWorki[0 q[?25h[?2026l]0;⠏ scratch[?2026h•[0 q[?25h[?2026l[?2026hWorkin[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠋ scratch[?2026hWorking[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠙ scratch[?2026hWorking5[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h•Working[0 q[?25h[?2026l]0;⠹ scratch[?2026h[0 q[?25h[?2026l[?2026hWorking[0 q[?25h[?2026l[?2026h•[0 q[?25h[?2026l]0;⠸ scratch[?2026horking[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h•rking[0 q[?25h[?2026l]0;⠼ scratch[?2026h[0 q[?25h[?2026l[?2026h•king[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠴ scratch[?2026h[0 q[?25h[?2026l[?2026h•ing[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠦ scratch[?2026hng[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hg[0 q[?25h[?2026l]0;⠧ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠇ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠏ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠋ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h6[0 q[?25h[?2026l]0;⠙ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠹ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠸ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠼ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠴ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hW[0 q[?25h[?2026l]0;⠦ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hWo[0 q[?25h[?2026l]0;⠧ scratch[?2026h•[0 q[?25h[?2026l[?2026hWor[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠇ scratch[?2026h•Work[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l[?2026hMM + +• OK  › Ask Codex to do anything  gpt-5.6-sol medium · ~/orca-lanes/sta8834/corpus/scratch[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;⠏ scratch[?2026h[0 q[?25h[?2026l[?2026h[0 q[?25h[?2026l]0;scratch[?2026h[0 q[?25h[?2026l \ No newline at end of file diff --git a/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.meta.json b/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.meta.json new file mode 100644 index 00000000000..ca6db1a3254 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.meta.json @@ -0,0 +1,14 @@ +{ + "capturedAt": "2026-09-29T01:44:01.149Z", + "platform": "darwin", + "command": [ + "codex", + "-c", + "check_for_update_on_startup=false", + "--dangerously-bypass-approvals-and-sandbox" + ], + "cols": 120, + "rows": 40, + "note": "codex-cli 0.155.1, default tui animations. One tool-using turn sent once the composer had been quiet 2.5s; cut 20s past the last busy frame, before exit. .timing.json holds each PTY chunk as [ms since spawn, UTF-16 length] and the prompt-submit time. STA-8834.", + "exitCode": 0 +} diff --git a/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.timing.json b/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.timing.json new file mode 100644 index 00000000000..8258e899ce0 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.timing.json @@ -0,0 +1 @@ +{"promptSentAtMs":4478.2,"chunks":[[63.6,8],[63.7,47],[164.3,17],[164.5,730],[164.6,8],[164.6,78],[164.8,78],[165.4,78],[171.5,78],[172.5,78],[174.7,17],[174.8,794],[174.9,132],[175.2,79],[175.7,79],[197.3,79],[197.5,79],[203.1,79],[205.8,71],[205.8,8],[206,71],[206.2,87],[636.8,79],[641.5,79],[642.3,12],[642.6,202],[642.8,71],[642.9,8],[671.8,79],[728.1,14],[749.8,31],[749.8,767],[749.9,506],[750.9,77],[792.1,12],[793.2,77],[995.7,72],[995.7,178],[1340.2,77],[3899.1,153],[4478.5,104],[4478.5,94],[4478.6,160],[4483.3,77],[4488.7,77],[4510.6,14],[4511.7,18],[4511.9,509],[4545.3,14],[4545.4,77],[4579.4,77],[4613,77],[4646.2,14],[4646.3,77],[4679.2,77],[4712.4,77],[4721.2,77],[4732.5,77],[4741.8,77],[4755.1,14],[4755.2,77],[4756.4,30],[4763.7,132],[4798.3,77],[4831.8,77],[4865.3,30],[4865.4,77],[4898,77],[4931.8,77],[4965.6,30],[4965.7,77],[4998.3,77],[5032,77],[5066.6,30],[5066.7,77],[5100.6,111],[5134.7,77],[5168.8,30],[5168.8,77],[5202.5,111],[5235.8,77],[5269.8,30],[5269.9,77],[5304,111],[5338,77],[5372.6,30],[5372.7,111],[5406.4,77],[5440.5,77],[5474.5,30],[5474.6,111],[5508.1,77],[5542.1,30],[5542.2,82],[5542.2,8],[5576.7,111],[5610.9,77],[5645.1,30],[5645.3,77],[5678.4,111],[5712.5,77],[5745.7,30],[5745.9,77],[5779,111],[5812.7,77],[5845.4,30],[5845.6,111],[5879.6,77],[5913.5,77],[5945.9,30],[5946,111],[5980.4,77],[6014.5,77],[6048.6,30],[6048.7,77],[6082,77],[6116.5,77],[6150.1,30],[6150.2,77],[6183.5,77],[6217.5,77],[6251.7,30],[6252.1,77],[6285.1,77],[6318.6,77],[6352.7,30],[6352.9,77],[6386.1,77],[6425.6,77],[6459.8,30],[6460,77],[6493.6,77],[6527.3,90],[6561.3,30],[6561.4,77],[6594.4,77],[6627.4,77],[6661,30],[6661.2,77],[6695,77],[6729.1,77],[6763.1,30],[6763.2,77],[6797.2,77],[6831.3,77],[6865.4,30],[6865.5,77],[6898.9,77],[6933.1,77],[6967.1,30],[6967.2,77],[7001.2,77],[7035.3,77],[7068.9,30],[7069,77],[7102.2,111],[7136.6,77],[7169.9,30],[7170.3,77],[7204.3,111],[7238.4,77],[7272.3,30],[7272.3,77],[7305.3,111],[7339,77],[7373,30],[7373.2,111],[7407.1,77],[7441.1,77],[7474.7,30],[7474.9,111],[7508.5,77],[7542.6,30],[7542.8,90],[7576.9,111],[7610,77],[7644.6,30],[7644.7,77],[7679,111],[7712.9,77],[7746.6,30],[7746.8,77],[7779.2,111],[7813,77],[7847,30],[7847.1,111],[7881.2,77],[7915.3,77],[7949.3,30],[7949.4,111],[7982.8,77],[8016.8,77],[8050.1,30],[8050.1,29],[8050.1,48],[8083.8,77],[8117.9,77],[8151.5,30],[8151.5,77],[8185.7,77],[8219.4,77],[8252.2,30],[8252.3,77],[8286.4,77],[8320.4,77],[8354.4,30],[8354.5,77],[8388,77],[8422,77],[8456,30],[8456,77],[8490.2,77],[8524.5,90],[8558.4,30],[8558.6,77],[8592.8,77],[8626.1,77],[8659.8,30],[8660.1,77],[8694.5,77],[8727.9,77],[8761.8,30],[8762,77],[8796.8,77],[8830.8,77],[8864.1,30],[8864.3,77],[8897.6,77],[8931.7,77],[8965.2,30],[8965.3,77],[8999.4,77],[9032.6,77],[9066.7,30],[9066.8,77],[9101.1,111],[9135.1,77],[9169,30],[9169.2,77],[9202.6,111],[9236.7,77],[9270.7,30],[9270.8,77],[9305,111],[9339.3,77],[9373.7,30],[9373.9,111],[9407.8,77],[9441,77],[9474.5,30],[9474.6,111],[9507,77],[9540.9,90],[9574.4,30],[9574.5,111],[9608.6,77],[9642.6,30],[9642.7,77],[9676.7,111],[9710.9,77],[9744.2,30],[9744.2,77],[9778.4,111],[9812.5,77],[9846.5,30],[9846.6,111],[9880.7,77],[9914.9,77],[9948.5,30],[9948.6,111],[9982.6,77],[10015.9,77],[10049.2,30],[10049.2,77],[10082.8,69],[10082.8,8],[10116.3,77],[10149.7,30],[10149.8,77],[10183.7,77],[10218.2,77],[10251,30],[10251.1,77],[10284.1,77],[10317.7,77],[10352.3,30],[10352.3,69],[10352.4,8],[10386.5,77],[10420.5,77],[10454.8,30],[10455.1,77],[10488.6,77],[10521.9,90],[10555.8,30],[10556.1,77],[10590.5,77],[10624.7,77],[10658.7,30],[10658.9,77],[10692.4,69],[10692.5,8],[10726.2,77],[10760.1,30],[10760.2,77],[10794.4,77],[10828.5,77],[10861.6,30],[10861.8,77],[10894.7,77],[10928.6,77],[10962.6,30],[10962.8,77],[10995.4,77],[11029.6,77],[11063.4,30],[11063.5,77],[11097.3,111],[11131.7,77],[11165,30],[11165.1,77],[11199.2,111],[11233.1,77],[11266.3,30],[11266.4,77],[11299.6,111],[11333.2,77],[11367.2,30],[11367.3,77],[11401.5,111],[11435.5,77],[11469.4,30],[11469.5,111],[11503.6,77],[11537.7,90],[11571.7,30],[11571.8,111],[11606,77],[11639.6,77],[11672.8,30],[11672.9,111],[11695.8,77],[11730.1,77],[11764.1,30],[11764.2,111],[11798.4,77],[11832.2,77],[11864.9,30],[11865,111],[11898.6,77],[11932.3,77],[11966.4,30],[11966.5,97],[11966.5,14],[12000.2,77],[12033.5,77],[12067.5,30],[12067.6,77],[12101.2,77],[12135.4,69],[12135.5,8],[12168.8,30],[12168.9,69],[12169,8],[12203.1,77],[12237.2,77],[12270.6,30],[12270.8,63],[12270.8,14],[12304.9,77],[12339.7,77],[12373.6,30],[12373.8,77],[12406.9,77],[12441.1,77],[12475.2,30],[12475.4,77],[12509.1,77],[12543.1,30],[12543.4,90],[12577.4,77],[12611.4,77],[12644.6,30],[12644.8,77],[12678.6,77],[12712.7,77],[12746.3,30],[12746.4,77],[12779.6,77],[12812.5,77],[12846.1,30],[12846.2,77],[12880.3,77],[12914.3,77],[12948.3,30],[12948.4,77],[12982.7,77],[13016.9,77],[13049.9,30],[13050.1,77],[13083.6,111],[13117.1,77],[13151.1,30],[13151.2,77],[13184.2,111],[13217.4,77],[13250.7,30],[13250.8,77],[13284.9,111],[13301.8,77],[13336.6,77],[13370.1,30],[13370.1,111],[13403.8,77],[13438.1,77],[13472,30],[13472.2,111],[13505.2,77],[13539.6,90],[13572.7,30],[13572.8,111],[13607.6,77],[13641.8,77],[13675.5,30],[13675.6,111],[13709.7,77],[13742.2,30],[13742.3,63],[13742.3,14],[13776.5,111],[13810.3,77],[13844.3,30],[13844.6,77],[13877.7,111],[13911.7,77],[13944.4,30],[13944.6,111],[13978,77],[14012.1,77],[14046.6,30],[14046.8,77],[14080.9,77],[14115,77],[14148.5,30],[14148.7,63],[14148.7,14],[14182.8,77],[14217,77],[14250.6,30],[14250.7,77],[14284.9,77],[14319,77],[14353.5,30],[14353.5,77],[14387.7,77],[14421.9,77],[14456,30],[14456.2,77],[14490.3,77],[14523.9,119],[14558.3,30],[14558.3,77],[14592.5,77],[14626.6,77],[14660,30],[14660,77],[14694.1,77],[14728,77],[14761.6,30],[14761.6,69],[14761.7,8],[14771.2,73],[14771.3,192],[14805.5,77],[14839.7,77],[14873.5,30],[14873.7,77],[14907.8,77],[14941.4,77],[14974,30],[14974.1,77],[15007.3,77],[15040.5,77],[15074.5,30],[15074.6,77],[15106.9,111],[15140.6,77],[15172.8,30],[15172.9,77],[15180,14],[15182.8,41],[15183,177],[15192.7,77],[15226.5,77],[15260.2,41],[15260.2,77],[15294.3,111],[15328.5,77],[15361.7,41],[15361.9,63],[15362,14],[15395.9,111],[15429.6,77],[15463.6,41],[15463.8,77],[15496.8,111],[15531.1,90],[15565,41],[15565.2,111],[15599.4,77],[15632.9,77],[15666.2,41],[15666.4,111],[15700.3,77],[15734.5,77],[15768.5,41],[15768.6,111],[15802.6,77],[15835.4,77],[15869.2,41],[15869.3,111],[15903.2,77],[15935.8,77],[15969.9,41],[15970,111],[16003.5,77],[16037.5,77],[16071.6,41],[16071.8,77],[16105.7,77],[16138.7,77],[16172.9,41],[16172.9,77],[16207,77],[16241.4,77],[16275.4,41],[16275.4,69],[16275.5,8],[16309.6,77],[16343.6,41],[16343.8,77],[16377.8,77],[16411.9,77],[16445.8,41],[16445.9,77],[16480.1,77],[16513.3,90],[16547.1,41],[16547.2,77],[16580.9,77],[16615.2,77],[16648.6,41],[16648.7,77],[16682,77],[16715.1,77],[16748.6,41],[16748.7,77],[16781.9,77],[16816.1,77],[16850.1,41],[16850.2,77],[16884.8,77],[16919,77],[16952.9,41],[16953,77],[16987.7,77],[17021.1,77],[17054.5,41],[17054.5,69],[17054.6,8],[17088.6,103],[17088.7,8],[17122.5,77],[17155.9,41],[17156,77],[17190.1,111],[17224.3,77],[17258.3,41],[17258.4,69],[17258.4,8],[17292.3,111],[17326.3,77],[17360.7,41],[17360.8,77],[17395.7,111],[17429.9,77],[17463.9,41],[17464.1,77],[17498,111],[17531.3,90],[17564.9,41],[17565.1,111],[17599.1,77],[17633.1,77],[17666.7,41],[17666.7,111],[17700.9,77],[17734.8,77],[17768.8,41],[17768.9,103],[17769,8],[17803,77],[17837.1,77],[17871.1,41],[17871.1,111],[17904.8,77],[17939,77],[17973.1,41],[17973.2,111],[18006.8,77],[18040.7,77],[18074.5,41],[18074.5,63],[18074.5,14],[18108.6,77],[18142,41],[18142.1,77],[18175.5,77],[18209.1,69],[18209.2,8],[18242.3,41],[18242.4,77],[18276.6,77],[18310.2,77],[18344.2,41],[18344.3,77],[18378.4,77],[18412.6,77],[18446.6,41],[18446.7,77],[18479.9,69],[18479.9,8],[18513.5,90],[18546.9,41],[18547,77],[18576.3,77],[18609.3,77],[18642.9,41],[18643,77],[18677.1,77],[18711.2,77],[18744.2,41],[18744.3,77],[18777.3,77],[18787.6,18],[18787.8,759],[18797.1,77],[18813.8,77],[18823.3,77],[18856.4,41],[18856.4,77],[18890.3,77],[18924.4,77],[18958.4,41],[18958.5,77],[18992.4,77],[19026.6,77],[19060.6,41],[19060.7,77],[19094.9,111],[19129,77],[19162.9,41],[19163,69],[19163,8],[19197.1,111],[19231.5,77],[19264.7,41],[19264.9,69],[19265,8],[19299.2,111],[19332.9,77],[19366.9,41],[19367.1,29],[19367.1,48],[19401.3,111],[19435.4,77],[19469.5,41],[19469.6,63],[19469.6,48],[19503.8,77],[19537,90],[19571,41],[19571.1,111],[19605.3,77],[19639.3,77],[19672.5,41],[19672.5,111],[19716,77],[19746.1,41],[19746.1,77],[19779.7,111],[19813.8,77],[19847.7,41],[19847.7,97],[19847.8,14],[19881.8,77],[19916,77],[19950.1,41],[19950.1,103],[19950.2,8],[19984.4,77],[20018.7,77],[20052.7,41],[20052.9,77],[20087.2,77],[20121.5,77],[20155.6,41],[20155.8,69],[20155.8,8],[20189,77],[20223.5,77],[20257.4,41],[20257.6,63],[20257.6,14],[20291.2,69],[20291.3,8],[20324.7,77],[20358.1,41],[20358.2,77],[20392.4,77],[20426.5,77],[20460.6,41],[20460.7,77],[20494.8,77],[20528.9,90],[20562.9,41],[20563,77],[20597.2,77],[20630.5,77],[20664.6,41],[20664.7,77],[20699.5,77],[20733.6,77],[20766.9,41],[20767,77],[20800.2,77],[20834.6,77],[20867.8,41],[20868,77],[20901.6,77],[20935.6,77],[20968.8,41],[20969,29],[20969.1,48],[21003.3,69],[21003.4,8],[21037.4,77],[21071.4,41],[21071.6,69],[21071.6,8],[21105.8,111],[21139.9,77],[21174,41],[21174.1,77],[21207.6,111],[21218.4,77],[21251.8,41],[21251.9,77],[21286.2,111],[21320.1,77],[21354.2,41],[21354.3,63],[21354.3,14],[21388.7,111],[21422.8,77],[21456.1,41],[21456.2,77],[21490.3,111],[21524.4,90],[21558.6,41],[21558.9,77],[21593.2,111],[21627.5,77],[21661.5,41],[21661.6,63],[21661.6,48],[21695.9,77],[21730,77],[21763.4,41],[21763.4,103],[21763.5,8],[21797.6,77],[21831.4,77],[21865.4,41],[21865.4,97],[21865.4,14],[21899.6,77],[21933.7,77],[21967.8,41],[21967.8,111],[22002,77],[22036.1,77],[22070.2,41],[22070.2,29],[22070.3,48],[22104.2,77],[22138.5,77],[22171.9,41],[22172.1,77],[22206.3,77],[22240.6,77],[22273.1,41],[22273.2,77],[22307.3,77],[22341.1,77],[22375,41],[22375.1,69],[22375.1,8],[22409.4,77],[22443.5,41],[22443.6,63],[22443.7,14],[22477.9,77],[22511.2,90],[22544.7,41],[22544.8,77],[22579,77],[22612.5,77],[22646.4,41],[22646.5,77],[22679.8,77],[22713.9,77],[22748.5,41],[22748.6,69],[22748.6,8],[22782.7,77],[22817,77],[22850.2,41],[22850.3,63],[22850.3,14],[22883.7,77],[22894.2,18],[22894.2,117],[22894.3,673],[22894.3,8],[22928.5,77],[22962,41],[22962,77],[22996.5,77],[23030.5,77],[23064.6,41],[23064.7,29],[23064.7,48],[23098.2,111],[23133,77],[23166.4,41],[23166.5,77],[23193.2,18],[23193.2,65],[23193.3,561],[23193.3,8],[23295.2,41],[23397.5,41],[23499.7,41],[23601.5,41],[23703.1,41],[23805.7,41],[23908.4,41],[24010.6,41],[24048,41],[24048.1,245],[24057.7,77],[24083.3,77],[24083.7,39],[24092.3,73],[24092.4,108],[24092.4,14]]} diff --git a/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.txt b/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.txt new file mode 100644 index 00000000000..6064224f724 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-155-1-timed-turn.txt @@ -0,0 +1,26 @@ +[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h╭───────────────────────────────────────╮│ >_ OpenAI Codex (v0.155.1) ││ ││ model: loading /model to change ││ directory: loading │╰───────────────────────────────────────╯  › Ask Codex to do anything  ? for shortcuts[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h╭──────────────────────────────────────────────────╮│ >_ OpenAI Codex (v0.155.1) ││ ││ model: loading /model to change ││ directory: ~/orca-lanes/sta8834/corpus/scratch ││ permissions: YOLO mode │╰──────────────────────────────────────────────────╯  › Ask Codex to do anything  ? for shortcuts[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l]0;scratch[?2026hgpt-6-sol default · ~/orca-lanes/sta8834/corpus/scratch[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l[?2026h[0 q╭[?25h[?2026l]0;⠋ scratch]0;⠙ scratch[?2026hMMMMMMMMM +╭──────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.155.1) │ +│ │ +│ model: gpt-6-sol medium /model to change │ +│ directory: ~/orca-lanes/sta8834/corpus/scratch │ +│ permissions: YOLO mode │ +╰──────────────────────────────────────────────────╯ + + Tip: New Use /fast to enable our fastest inference with increased plan usage.  › Ask Codex to do anything  gpt-6-sol medium · ~/orca-lanes/sta8834/corpus/scratch[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;scratch[?2026h[0 q [?25h[?2026l[?2026hMM + +• You have 3 usage limit resets available. Run /usage to use one.[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026hList the files here and summarizein2bullets.[0 q [?25h[?2026l[?2026hMMMM + + +› List the files here and summarize in 2 bullets. +Ask Codex to do anything[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ scratch[?2026h • Working (0s • esc to interrupt)  › Ask Codex to do anything  gpt-6-sol medium · ~/orca-lanes/sta8834/corpus/scratch[0 q [?25h[?2026l]0;⠏ scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ scratch[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h · renaming... ⠙[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h1[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h2[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h3[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h4[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h5[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h6[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h7[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h8[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h9[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ renaming... ⠦ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ renaming... ⠧ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ renaming... ⠇ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h10s • esc to interupt)[0 q [?25h[?2026l]0;⠏ renaming... ⠏ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ renaming... ⠋ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ renaming... ⠙ | scratch[?2026h[0 q [?25h[?2026l[?2026hMM + +• I’ll inspect the current directory and summarize what’s there in two bullets.[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ renaming... ⠹ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ renaming... ⠸ | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ renaming... ⠼ | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ renaming... ⠴ | scratch[?2026h[0 q [?25h[?2026l]0;⠴ scratch]0;⠴ List and summarize files | scratch[?2026h•List ad summarize files[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h1[0 q [?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h2[0 q [?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h3[0 q [?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h4[0 q [?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h • Explored └ List rg --files -g '*' -g '!*/.git/*' • Working (14s • esc to interrupt)  › Ask Codex to do anything  gpt-6-sol medium · ~/orca-lanes/sta8834/corpus/scratch · List and summarize files[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h5[0 q [?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h6[0 q [?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h7[0 q [?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h8[0 q [?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026hMMM + +• Explored + └ List rg --files -g '*' -g '!*/.git/*' • Working (18s • esc to interrupt)  › Ask Codex to do anything  gpt-6-sol medium · ~/orca-lanes/sta8834/corpus/scratch · List and summarize files[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026h•[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[0 q [?25h[?2026l[?2026hMM + +• - README.md and notes.txt are the two working files.  › Ask Codex to do anything  gpt-6-sol medium · ~/orca-lanes/sta8834/corpus/scratch · List and summarize files[0 q [?25h[?2026l]0;⠦ List and summarize files | scratch]0;⠧ List and summarize files | scratch]0;⠇ List and summarize files | scratch]0;⠏ List and summarize files | scratch]0;⠋ List and summarize files | scratch]0;⠙ List and summarize files | scratch]0;⠹ List and summarize files | scratch]0;⠸ List and summarize files | scratch]0;⠼ List and summarize files | scratch[?2026hM + - A .git/ directory is present, containing repository metadata and history.[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l[?2026h[0 q [?25h[?2026l]0;List and summarize files | scratch[?2026hMM + + done 9:43 PM[0 q [?25h[?2026l \ No newline at end of file diff --git a/src/main/runtime/__fixtures__/codex-0-157-1-update-dialog.meta.json b/src/main/runtime/__fixtures__/codex-0-157-1-update-dialog.meta.json new file mode 100644 index 00000000000..f43deccb966 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-157-1-update-dialog.meta.json @@ -0,0 +1,9 @@ +{ + "capturedAt": "2026-09-29T01:41:43.328Z", + "platform": "darwin", + "command": ["codex", "--no-daemon", "--dangerously-bypass-approvals-and-sandbox"], + "cols": 120, + "rows": 40, + "note": "codex-cli 0.157.1: launch with the update check on; the update dialog owns the screen. STA-8834.", + "exitCode": 0 +} diff --git a/src/main/runtime/__fixtures__/codex-0-157-1-update-dialog.txt b/src/main/runtime/__fixtures__/codex-0-157-1-update-dialog.txt new file mode 100644 index 00000000000..4bd32773735 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-157-1-update-dialog.txt @@ -0,0 +1 @@ +[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[?2026h[?25l╭───────────────────────────────────────╮│ >_ OpenAI Codex (v0.157.1) ││ ││ model: loading /model to change ││ directory: loading │╰───────────────────────────────────────╯›Ask Codex to do anything?forshortcuts[0 q╭[?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l───────────╮ │ │ loading /model to change │ ~/orca-lanes/sta8834/corpus/scratch ││ permissions: YOLO mode │╰──────────────────────────────────────────────────╯[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l  Updat available · 0.157.1→0.158.0 Releasenotes:]8;;https://github.com/openai/codex/releases/latesthttps://github.com/openai/codex/releases/latest]8;; › 1. Update now (runs `npm install -g @openai/codex`)  2. Skip 3. Skip until next versionenter continue · esc skip [?2026l \ No newline at end of file diff --git a/src/main/runtime/__fixtures__/codex-0-158-0-approval.meta.json b/src/main/runtime/__fixtures__/codex-0-158-0-approval.meta.json new file mode 100644 index 00000000000..a333bd29473 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-158-0-approval.meta.json @@ -0,0 +1,18 @@ +{ + "capturedAt": "2026-09-29T01:43:13.919Z", + "platform": "darwin", + "command": [ + "codex", + "--no-daemon", + "-c", + "check_for_update_on_startup=false", + "-a", + "on-request", + "-s", + "read-only" + ], + "cols": 120, + "rows": 40, + "note": "codex-cli 0.158.0: read-only sandbox with on-request approvals; a command approval dialog owns the screen at the end. STA-8834.", + "exitCode": 0 +} diff --git a/src/main/runtime/__fixtures__/codex-0-158-0-approval.txt b/src/main/runtime/__fixtures__/codex-0-158-0-approval.txt new file mode 100644 index 00000000000..925a5165fc5 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-158-0-approval.txt @@ -0,0 +1 @@ +[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[?2026h[?25l>_ OpenAI Codex (v0.158.0)loadingAll right. What have you got?⣀⣤⣤⣤⣀⣀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄⣀⣾⣿⣿⠃⣀⣴⣾⣿⣿⡿⠟⠋⣀⡀⠈⠙⢿⣿⣿⣧⣀⣶⣿⣿⣿⣿⡇⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇⣰⣿⣿⡿⢿⣿⣿⡇⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷⢰⣿⣿⡟⠁⢸⣿⣿⡇⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀⣾⣿⣿⠃⢸⣿⣿⡇⢸⣿⣿⠋⠁⠈⠙⢻⣿⣿⣿⣶⣤⡀⠹⢿⣿⣷⡄⢸⣿⣿⣇⠸⣿⣿⣷⣦⣼⣿⣿⢸⣿⣿⠻⢿⣿⣿⡇⠘⣿⣿⣿⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀⢀⣠⣼⣿⣿⣿⣿⡇⣿⣿⣿⠇⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⡇⣠⣿⣿⣿⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿⣿⣿⣷⣴⣿⣿⣿⠃⠘⣿⣿⣷⠈⠛⢿⣿⣿⣿⠿⠋⠉⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠻⣿⣿⣿⣤⣀⠈⠉⢀⣤⣶⣿⣿⣿⠿⠟⠁⣰⣿⣿⡟⠋⠁⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁⣠⣼⣿⣿⡟⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏⠈⠙⠛⠿⠿⠿⠿⠛⠉›Ask Codex to do anything?forshortcuts[0 q [?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l~/orca-lanes/sta8834/corpus/scratch[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;scratch[?2026h[?25lGPT-6-Sol default·~/orca-lanes/sta8834/corpus/scratch[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ scratch]0;⠙ scratch[?2026h[?25lmdim · ~/orca-lanes/sta834/corpus/scratch[?25h[?2026l[?2026h[?25h[?2026l]0;scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lRun `touch approved.txt`.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l› Run`touchapproved.txt`.Ask Codex to do anything?forshortcuts[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ scratch[?2026h[?25l•Working(0s • esc to interrupt)[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ scratch[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25l·⠙[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25l1[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25l2[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25l3[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25l4[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25l5[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25l6[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25l7[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l8[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25l9[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25l• I [?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l’ll[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25lcreate[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25l⠴[?25h[?2026l[?2026h[?25lthe[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25l⠦[?25h[?2026l[?2026h[?25lfile[?25h[?2026l[?2026h[?25linthecurrent[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25l⠧[?25h[?2026l[?2026h[?25lworkspace;[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25l⠇[?25h[?2026l[?2026h[?25lthis[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25lrequires[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25l⠋[?25h[?2026l[?2026h[?25lwrite[?25h[?2026l[?2026h[?25laccess[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25l⠙[?25h[?2026l[?2026h[?25lbeyond[?25h[?2026l[?2026h[?25lthe[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25l⠹[?25h[?2026l[?2026h[?25lread-only[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25lsandbox.[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25l⠼[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25l⠴[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25l⠦[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25l⠧[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25l⠇[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25l⠏[?25h[?2026l[?2026h[?25l•Working(11s • esc to interrupt)[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25l2[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25l3[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;[ . ] Action Required | ⠸ | scratch[?2026h[?25l•Runningtouch approved.txtWould you like to run the following command?Environment:localReason:May I create approved.txt in the current workspace as requested?$touch approved.txt › 1. Yes, proceed (y)  2. Yes, and don't ask againforcommandsthatstartwith`touchapproved.txt`(p)3.No,andtellCodexwhattododifferently(esc)Press enter to confirm or esc to cancel [?2026l]0;[ . ] Action Required | ⠼ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠴ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠦ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠧ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠇ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠏ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠋ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠙ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠹ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠸ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠼ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠴ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠦ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠧ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠇ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠏ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠋ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠙ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠹ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠸ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠼ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠴ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠦ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠧ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠇ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠏ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠋ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠙ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠹ | scratch]0;[ ! ] Action Required | ⠸ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠼ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠴ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠦ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠧ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠇ | scratch[?2026h[?2026l]0;[ ! ] Action Required | ⠏ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠋ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠙ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠹ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠸ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠼ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠴ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠦ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠧ | scratch[?2026h[?2026l]0;[ . ] Action Required | ⠇ | scratch[?2026h[?2026l]0;[ . ] Action Required | scratch]0;[ . ] Action Required | Create approved.txt | scratch[?2026h[?2026l]0;[ ! ] Action Required | Create approved.txt | scratch]0;[ . ] Action Required | Create approved.txt | scratch]0;[ ! ] Action Required | Create approved.txt | scratch]0;[ . ] Action Required | Create approved.txt | scratch]0;[ ! ] Action Required | Create approved.txt | scratch]0;[ . ] Action Required | Create approved.txt | scratch]0;[ ! ] Action Required | Create approved.txt | scratch]0;[ . ] Action Required | Create approved.txt | scratch]0;[ ! ] Action Required | Create approved.txt | scratch]0;[ . ] Action Required | Create approved.txt | scratch]0;[ ! ] Action Required | Create approved.txt | scratch]0;[ . ] Action Required | Create approved.txt | scratch]0;[ ! ] Action Required | Create approved.txt | scratch \ No newline at end of file diff --git a/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.meta.json b/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.meta.json new file mode 100644 index 00000000000..2c3defda30a --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.meta.json @@ -0,0 +1,15 @@ +{ + "capturedAt": "2026-09-29T01:46:36.717Z", + "platform": "darwin", + "command": [ + "codex", + "--no-daemon", + "-c", + "check_for_update_on_startup=false", + "--dangerously-bypass-approvals-and-sandbox" + ], + "cols": 120, + "rows": 40, + "note": "codex-cli 0.158.0, default tui animations. One tool-using turn sent once the composer had been quiet 2.5s; cut 20s past the last busy frame, before exit. .timing.json holds each PTY chunk as [ms since spawn, UTF-16 length] and the prompt-submit time. STA-8834.", + "exitCode": 0 +} diff --git a/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.timing.json b/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.timing.json new file mode 100644 index 00000000000..02745d043e1 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.timing.json @@ -0,0 +1 @@ +{"promptSentAtMs":7226.9,"chunks":[[70.2,28],[70.4,27],[321.3,80],[321.5,9],[324.7,14],[324.8,578],[325.8,494],[327,43],[329.1,43],[331.5,43],[333.5,43],[335.5,43],[340.4,216],[340.5,8],[342.3,43],[344,43],[346,43],[372.5,43],[374.1,43],[385.8,43],[1330.3,43],[1332.2,35],[1332.2,8],[1334.4,43],[1336.3,43],[1338.5,43],[1340.5,43],[2820.3,43],[2829.5,43],[2831.2,12],[2833.5,43],[2835.6,169],[2835.7,8],[2842.6,43],[2866,43],[2922.5,14],[2944.6,14],[2946.2,14],[2946.2,180],[2947.8,43],[2949.3,43],[3006.8,12],[3009.4,43],[3941,43],[4089.1,43],[6627.2,43],[6627.3,377],[6627.3,14],[6627.4,35],[6627.4,8],[6627.6,43],[6627.7,43],[6627.8,43],[6627.9,43],[6628,43],[6628.2,43],[6628.3,43],[6628.4,35],[6628.4,8],[6628.5,35],[6628.5,8],[6628.6,43],[6628.7,43],[6628.9,43],[6629,43],[6629.1,43],[6629.2,43],[6629.3,43],[6629.4,35],[6629.4,8],[6629.5,43],[6629.7,43],[6629.8,43],[6629.9,35],[6629.9,8],[6630,35],[6630,8],[6630.1,43],[6630.2,35],[6630.3,8],[6630.4,43],[6630.5,35],[6630.5,8],[6630.6,35],[6630.6,8],[6630.7,43],[6630.8,35],[6630.9,8],[6630.9,43],[6631.1,43],[6631.2,35],[6631.2,8],[6631.3,35],[6631.3,8],[6631.4,35],[6631.4,8],[6631.5,35],[6631.5,8],[6631.6,35],[6631.6,8],[6631.7,35],[6631.7,8],[6631.9,35],[6631.9,8],[6632,35],[6632,8],[6632.1,43],[6632.2,35],[6632.2,8],[6632.3,43],[6632.4,43],[6632.5,43],[6659.2,117],[6659.3,8],[7227.9,44],[7228.6,138],[7228.7,14],[7229,161],[7229.2,8],[7239.4,43],[7247.7,43],[7271.8,14],[7273,163],[7273.1,8],[7306.6,43],[7339.4,14],[7339.5,35],[7339.6,8],[7374.3,43],[7407.9,43],[7441.3,14],[7441.5,43],[7474.6,43],[7490.5,43],[7513.7,43],[7535.2,14],[7535.3,35],[7535.3,8],[7536.4,18],[7545.7,89],[7578.7,43],[7611.6,43],[7646,18],[7646.1,43],[7679.3,43],[7713.6,43],[7747,18],[7747.2,35],[7747.2,8],[7780,43],[7813.5,43],[7846.7,18],[7846.8,75],[7846.9,8],[7879.7,43],[7912.9,43],[7946.5,18],[7946.7,83],[7980.7,43],[8015.1,43],[8048.9,18],[8049.1,83],[8083.6,43],[8116.9,43],[8150.3,18],[8150.5,69],[8150.6,14],[8185.3,43],[8219.4,35],[8219.5,8],[8253.4,18],[8253.5,75],[8253.5,8],[8287.5,62],[8321.7,83],[8354.9,18],[8355,43],[8388.5,43],[8422.7,83],[8456.2,18],[8456.4,35],[8456.4,8],[8489.6,43],[8522.9,83],[8534.4,18],[8534.6,43],[8568.8,43],[8603.1,43],[8637.1,18],[8637.4,69],[8637.4,14],[8672.2,43],[8706.4,83],[8738.4,18],[8738.6,43],[8772.8,43],[8806.7,43],[8840,18],[8840.2,35],[8840.3,8],[8874.6,43],[8908,43],[8941.4,18],[8941.7,35],[8941.7,8],[8976.8,43],[9010.8,43],[9044.4,18],[9044.7,43],[9077.8,43],[9112.4,43],[9146.4,18],[9146.5,35],[9146.5,8],[9179.7,43],[9213.5,43],[9247,18],[9247.2,35],[9247.2,8],[9281.4,54],[9281.4,8],[9315.9,43],[9349.2,18],[9349.4,35],[9349.4,8],[9383.7,43],[9418,43],[9446.6,18],[9446.8,90],[9447,8],[9456.9,43],[9493.4,63],[9502.6,60],[9519.3,82],[9534.9,18],[9535.1,52],[9535.2,8],[9555.5,58],[9555.7,8],[9575.4,61],[9597.6,59],[9619.5,64],[9629.8,51],[9629.9,8],[9643,18],[9643.2,52],[9643.3,8],[9668.1,56],[9668.4,8],[9684.5,58],[9786.6,18],[9788.1,72],[9788.2,8],[9889.3,18],[9890.6,80],[9918.4,171],[9953.3,101],[9985.2,43],[10018.5,43],[10051.6,18],[10051.8,75],[10051.8,8],[10085.6,43],[10119.4,43],[10152.9,18],[10153.1,75],[10153.1,8],[10186.5,43],[10220.8,43],[10254.6,18],[10254.8,69],[10254.9,14],[10289.8,54],[10290,8],[10324.3,83],[10357.6,18],[10357.9,43],[10391.4,43],[10424,83],[10457.5,18],[10457.7,35],[10457.7,8],[10491,43],[10524.9,83],[10558.6,18],[10559,35],[10559.1,8],[10592.5,43],[10626.9,83],[10660.9,18],[10661.1,43],[10694.8,43],[10729.4,83],[10763.4,18],[10763.5,43],[10798.4,43],[10833.1,18],[10833.2,43],[10866.5,43],[10900.5,43],[10933.6,18],[10933.7,43],[10966.9,43],[11000.6,43],[11034.6,18],[11034.9,43],[11069.1,43],[11103,43],[11137.1,18],[11137.2,35],[11137.3,8],[11169.2,43],[11199,43],[11231.3,18],[11231.4,43],[11260.7,338],[11260.8,8],[11270.5,43],[11286.1,62],[11296.4,72],[11296.5,8],[11330.4,43],[11364.5,18],[11364.7,43],[11398.1,43],[11432.1,18],[11432.3,35],[11432.3,8],[11465.9,43],[11498.9,43],[11532.8,18],[11533,35],[11533,8],[11566.1,43],[11598.9,43],[11633,18],[11633.2,35],[11633.2,8],[11667.1,43],[11700.9,43],[11735,18],[11735.2,43],[11769.5,43],[11802.8,43],[11835.7,18],[11835.9,35],[11835.9,8],[11869.6,83],[11903.7,43],[11937.8,18],[11938.1,35],[11938.1,8],[11961.5,119],[11962.2,14],[11965.3,41],[11970.6,103],[12004.5,43],[12038.7,41],[12038.9,75],[12039,8],[12073.3,43],[12107.2,43],[12141,41],[12141.3,14],[12141.4,69],[12174.7,43],[12208.4,43],[12241.5,41],[12241.8,14],[12241.8,69],[12276.1,62],[12309.7,43],[12343.6,41],[12343.7,69],[12343.8,14],[12378,43],[12412.2,43],[12445.3,41],[12445.4,75],[12445.4,8],[12479.1,43],[12512.6,75],[12512.7,8],[12545.6,41],[12545.8,35],[12545.9,8],[12579.3,43],[12612.8,83],[12646.7,41],[12647,35],[12647,8],[12664.5,43],[12698.5,43],[12731.6,41],[12732,14],[12732,69],[12766.1,43],[12799.5,43],[12833.8,41],[12834.3,35],[12834.3,8],[12867.9,43],[12902.4,43],[12935.2,41],[12935.7,35],[12935.8,8],[12969.7,43],[13003.8,43],[13037.5,41],[13037.7,43],[13071.9,43],[13104.8,43],[13137.8,41],[13138,43],[13171.4,43],[13205.7,43],[13240.2,41],[13240.3,43],[13274.4,62],[13307.9,43],[13341.3,41],[13341.5,35],[13341.5,8],[13376.5,43],[13409.8,43],[13443.1,41],[13443.3,43],[13476.8,43],[13510.5,43],[13544.3,41],[13544.5,35],[13544.5,8],[13578.7,43],[13612.2,43],[13645.2,41],[13645.4,35],[13645.4,8],[13679.6,43],[13714.4,43],[13747.5,41],[13747.8,35],[13747.9,8],[13781.8,43],[13814.6,43],[13848,41],[13848.3,69],[13848.3,14],[13881.6,43],[13916.7,43],[13950.2,41],[13950.4,69],[13950.4,14],[13984.6,43],[14018.3,43],[14052.6,41],[14052.7,69],[14052.7,14],[14086.4,43],[14120.6,43],[14154.9,41],[14155,69],[14155,14],[14181.8,43],[14211.3,43],[14244.5,41],[14244.7,75],[14244.7,8],[14277.6,54],[14277.7,8],[14287.3,273],[14287.3,8],[14305.4,43],[14315.6,43],[14349.5,41],[14349.6,75],[14349.7,8],[14383.6,43],[14417.1,75],[14417.2,8],[14451.1,41],[14451.3,35],[14451.3,8],[14485.5,43],[14520.1,75],[14520.2,8],[14554.3,41],[14554.5,35],[14554.6,8],[14589.7,43],[14624.4,83],[14656.7,41],[14657,35],[14657,8],[14691.2,43],[14724.7,83],[14758.2,41],[14758.3,35],[14758.3,8],[14792.3,43],[14826.5,43],[14859.9,41],[14860,35],[14860,8],[14893.7,43],[14927.8,43],[14961.2,41],[14961.4,43],[14971.2,43],[14981.8,89],[14981.8,14],[14992.9,54],[14993,8],[15014.7,52],[15014.8,8],[15038.6,41],[15038.8,93],[15038.9,14],[15060.8,50],[15060.8,8],[15078.9,68],[15078.9,8],[15100.6,56],[15100.7,8],[15120.6,50],[15120.6,8],[15148.5,41],[15148.8,56],[15148.8,8],[15158.6,51],[15158.6,8],[15211.6,65],[15223,53],[15223.1,8],[15232,41],[15232.3,53],[15232.3,8],[15333.7,41],[15435.2,41],[15436.8,14],[15436.9,136],[15447.1,59],[15485.9,43],[15524.5,43],[15534.6,41],[15534.7,43],[15551,43],[15551.2,39],[15559.7,180]]} diff --git a/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.txt b/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.txt new file mode 100644 index 00000000000..f589f998bb8 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-158-0-timed-turn.txt @@ -0,0 +1 @@ +[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[?2026h[?25l>_ OpenAI Codex (v0.158.0)loadingShall we see where this goes?⣀⣤⣤⣤⣀⣀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄⣀⣾⣿⣿⠃⣀⣴⣾⣿⣿⡿⠟⠋⣀⡀⠈⠙⢿⣿⣿⣧⣀⣶⣿⣿⣿⣿⡇⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇⣰⣿⣿⡿⢿⣿⣿⡇⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷⢰⣿⣿⡟⠁⢸⣿⣿⡇⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀⣾⣿⣿⠃⢸⣿⣿⡇⢸⣿⣿⠋⠁⠈⠙⢻⣿⣿⣿⣶⣤⡀⠹⢿⣿⣷⡄⢸⣿⣿⣇⠸⣿⣿⣷⣦⣼⣿⣿⢸⣿⣿⠻⢿⣿⣿⡇⠘⣿⣿⣿⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀⢀⣠⣼⣿⣿⣿⣿⡇⣿⣿⣿⠇⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⡇⣠⣿⣿⣿⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿⣿⣿⣷⣴⣿⣿⣿⠃⠘⣿⣿⣷⠈⠛⢿⣿⣿⣿⠿⠋⠉⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠻⣿⣿⣿⣤⣀⠈⠉⢀⣤⣶⣿⣿⣿⠿⠟⠁⣰⣿⣿⡟⠋⠁⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁⣠⣼⣿⣿⡟⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏⠈⠙⠛⠿⠿⠿⠿⠛⠉›Ask Codex to do anything?forshortcuts[0 q [?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l~/orca-lanes/sta8834/corpus/scratch permissions: YOLO modeShall we see where this goes?[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;scratch[?2026h[?25h[?2026l[?2026h[?25lGPT-6-Sol default·~/orca-lanes/sta8834/corpus/scratch[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ scratch]0;⠙ scratch[?2026h[?25lmdim · ~/orca-lanes/sta834/corpus/scratch[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lList the files here and summarizein2bullets.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25lAsk Codex to do anything?forshortcuts[?25h[?2026l[?2026h[?25l› Listthefileshereandsummarizein2bullets.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ scratch[?2026h[?25l•Working(0s • esc to interrupt)[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ scratch[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25l·⠧[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25l1[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25l2[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25l• I’ll [?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25linspect[?25h[?2026l[?2026h[?25lthe[?25h[?2026l[?2026h[?25ldirectorycontents[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25land[?25h[?2026l[?2026h[?25lsummarize[?25h[?2026l[?2026h[?25lwhat[?25h[?2026l[?2026h[?25l’s[?25h[?2026l[?2026h[?25lpresent[?25h[?2026l[?2026h[?25lin[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25ltwo[?25h[?2026l[?2026h[?25lbullets[?25h[?2026l[?2026h[?25l.[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25l⠏[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25l⠋[?25h[?2026l[?2026h[?25l•Working(2s • esc to interrupt)[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25l3[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ ⠹ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ ⠸ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ ⠼ | scratch[?2026h[?25h[?2026l[?2026h[?25l•Explored └ Listrg--files-g'*'-g'.*' + Show details [?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l4[?25h[?2026l[?2026h[?25l⠼[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ ⠴ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ ⠦ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ ⠧ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ ⠇ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ ⠏ | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ ⠋ | scratch[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ ⠙ | scratch[?2026h[?25h[?2026l[?2026h[?25l•⠙[?25h[?2026l]0;⠙ scratch]0;⠙ List and summarize files | scratch[?2026h[?25lList and summarize files[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25l5[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25l6[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25l7[?25h[?2026l[?2026h[?25l Read README.md,notes.txt + Show details [?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ List and summarize files | scratch[?2026h[?25l•[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠦ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠇ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l•[?25h[?2026l]0;⠏ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠋ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠙ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l• •` [?25h[?2026l[?2026h[?25lREADME[?25h[?2026l[?2026h[?25l.md[?25h[?2026l]0;⠹ List and summarize files | scratch[?2026h[?25lREADME.md[?25h[?2026l[?2026h[?25l—[?25h[?2026l[?2026h[?25lcontainsthe[?25h[?2026l[?2026h[?25lheading[?25h[?2026l[?2026h[?25l“[?25h[?2026l]0;⠸ List and summarize files | scratch[?2026h[?25lscratch[?25h[?2026l[?2026h[?25l.”[?25h[?2026l[?2026h[?25l•`[?25h[?2026l[?2026h[?25lnotes[?25h[?2026l]0;⠼ List and summarize files | scratch[?2026h[?25l.txt[?25h[?2026l]0;⠴ List and summarize files | scratch]0;⠦ List and summarize files | scratch[?2026h[?25lnotes.txt —containsthetext“hello[?25h[?2026l[?2026h[?25l.”[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ List and summarize files | scratch[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;List and summarize files | scratch[?2026h[?25l 9:46 PM [?25h[?2026l \ No newline at end of file diff --git a/src/main/runtime/__fixtures__/codex-0-158-0-trustprompt.meta.json b/src/main/runtime/__fixtures__/codex-0-158-0-trustprompt.meta.json new file mode 100644 index 00000000000..a80d88f5af9 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-158-0-trustprompt.meta.json @@ -0,0 +1,15 @@ +{ + "capturedAt": "2026-09-29T01:32:55.701Z", + "platform": "darwin", + "command": [ + "codex", + "--no-daemon", + "-c", + "check_for_update_on_startup=false", + "--dangerously-bypass-approvals-and-sandbox" + ], + "cols": 120, + "rows": 40, + "note": "codex-cli 0.158.0: launch in an untrusted folder; the recording ends on the unanswered trust dialog. STA-8834.", + "exitCode": 0 +} diff --git a/src/main/runtime/__fixtures__/codex-0-158-0-trustprompt.txt b/src/main/runtime/__fixtures__/codex-0-158-0-trustprompt.txt new file mode 100644 index 00000000000..fd002b0fe04 --- /dev/null +++ b/src/main/runtime/__fixtures__/codex-0-158-0-trustprompt.txt @@ -0,0 +1 @@ +[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[?2026h[?25l>_ OpenAI Codex (v0.158.0)loadingHello, world. Hello, you.⣀⣤⣤⣤⣀⣀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄⣀⣾⣿⣿⠃⣀⣴⣾⣿⣿⡿⠟⠋⣀⡀⠈⠙⢿⣿⣿⣧⣀⣶⣿⣿⣿⣿⡇⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇⣰⣿⣿⡿⢿⣿⣿⡇⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷⢰⣿⣿⡟⠁⢸⣿⣿⡇⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀⣾⣿⣿⠃⢸⣿⣿⡇⢸⣿⣿⠋⠁⠈⠙⢻⣿⣿⣿⣶⣤⡀⠹⢿⣿⣷⡄⢸⣿⣿⣇⠸⣿⣿⣷⣦⣼⣿⣿⢸⣿⣿⠻⢿⣿⣿⡇⠘⣿⣿⣿⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀⢀⣠⣼⣿⣿⣿⣿⡇⣿⣿⣿⠇⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⡇⣠⣿⣿⣿⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿⣿⣿⣷⣴⣿⣿⣿⠃⠘⣿⣿⣷⠈⠛⢿⣿⣿⣿⠿⠋⠉⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠻⣿⣿⣿⣤⣀⠈⠉⢀⣤⣶⣿⣿⣿⠿⠟⠁⣰⣿⣿⡟⠋⠁⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁⣠⣼⣿⣿⡟⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏⠈⠙⠛⠿⠿⠿⠿⠛⠉›Ask Codex to do anything?forshortcuts[0 q [?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l~/orca-lanes/sta8834/corpus/scratch permissions: YOLO modeHello, world. Hello, you.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?1006l[?1015l[?1003l[?1002l[?1000l[?1007h[?2026h[?25l Folder access/Users/xxxxxx/orca-lanes/sta8834/corpus/scratch  Trustthisfolder?Codexcanread,edit,andrunfileshere,subjecttoyourpermissionsettings.Foldersettingscan run code automatically,evenwithoutamodelrequest.Continueonlyifyoutrustthesefiles.Yourtrustdecisionwillbesaved.› 1. Trust and continue 2.Quitenter continue · esc quit [?2026l \ No newline at end of file diff --git a/src/main/runtime/acknowledged-terminal-tab-retirement-fixture.ts b/src/main/runtime/acknowledged-terminal-tab-retirement-fixture.ts index 51cd8b4cd8b..12b3340d2ac 100644 --- a/src/main/runtime/acknowledged-terminal-tab-retirement-fixture.ts +++ b/src/main/runtime/acknowledged-terminal-tab-retirement-fixture.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../shared/constants' +import type { PersistedState } from '../../shared/persisted-state-types' import type { RuntimeSyncWindowGraph } from '../../shared/runtime-types' import { closeTerminalTabInWorkspaceSession } from '../../shared/workspace-session-terminal-tab-close' import { ProfileStateSqliteAuthority } from '../persistence/profile-state/profile-state-sqlite-authority' @@ -30,8 +31,9 @@ function deferred(): { promise: Promise; resolve: () => void } { export function createAcknowledgedTabRetirementFixture(bound = false) { const directory = mkdtempSync(join(tmpdir(), 'orca-close-ack-')) + const databasePath = join(directory, 'profile-state.db') const authority = new DelayedAuthority( - new ProfileStateSqliteAuthority(join(directory, 'profile-state.db'), 'ack-retirement') + new ProfileStateSqliteAuthority(databasePath, 'ack-retirement') ) const store = new Store({ dataFile: join(directory, 'orca-data.json'), @@ -149,6 +151,7 @@ export function createAcknowledgedTabRetirementFixture(bound = false) { store.setWorkspaceSession( advanceTerminalTopologyRevision(store.getWorkspaceSession(), ACK_WORKTREE) ) + let finalFlush: Promise | undefined const entered = deferred() const acknowledgement = deferred() const closeTerminalTab = vi.fn(async () => { @@ -184,15 +187,30 @@ export function createAcknowledgedTabRetirementFixture(bound = false) { acknowledgement, closeTerminalTab, publish, + /** Reads what a relaunch would load, independent of the store's in-memory state. */ + readDisk: (): PersistedState => { + const reader = new ProfileStateSqliteAuthority(databasePath, 'ack-retirement') + try { + return JSON.parse(reader.readSerializedState() ?? '{}') + } finally { + reader.close() + } + }, hasTab: () => store.getWorkspaceSession().tabsByWorktree[ACK_WORKTREE].some((tab) => tab.id === ACK_TAB), close: (options: { force?: boolean } = {}) => runtime.closeMobileSessionTab(`id:${ACK_WORKTREE}`, ACK_TAB, { reason: 'user', ...options }), + /** The app-quit flush; it finalizes persistence, so dispose must not flush again. */ + quit: () => (finalFlush ??= store.flushFinalOrThrowAsync()), dispose: async () => { runtime.setNotifier(null) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [] }) - await store.flushPendingOrThrowAsync() - await store.freezeWritesAsync() + if (finalFlush) { + await finalFlush + } else { + await store.flushPendingOrThrowAsync() + await store.freezeWritesAsync() + } setRuntimeDesktopSurface(null) rmSync(directory, { recursive: true, force: true }) } diff --git a/src/main/runtime/agent-session-acquisition-failure-settlement.ts b/src/main/runtime/agent-session-acquisition-failure-settlement.ts index a3e11408118..f6efaa890c3 100644 --- a/src/main/runtime/agent-session-acquisition-failure-settlement.ts +++ b/src/main/runtime/agent-session-acquisition-failure-settlement.ts @@ -86,12 +86,14 @@ export function settleFailedAgentSessionPostAcquisitionAttachment( } const next = args.exitProof === 'unproven' - ? withLease(record, { - ...record.lease, - handoffStage: 'recovering', - handoffOperationId: null, - lastRenewedAt: args.now - }) + ? { + ...withLease(record, { + ...record.lease, + handoffStage: 'recovering', + handoffOperationId: null + }), + updatedAt: args.now + } : withLease(record, { ...record.lease, runtimeFence: nextAgentSessionFence(record.lease), @@ -101,18 +103,15 @@ export function settleFailedAgentSessionPostAcquisitionAttachment( claimStatus: 'released', lastRenewedAt: args.now, handoffOperationId: null, - deathEvidence: - args.exitProof === 'root-exit-observed' - ? { - kind: 'exit-observed', - detail: 'the provider process exited; its descendants were not proven gone', - observedAt: args.now - } - : { - kind: 'exit-observed', - detail: 'post-acquisition cleanup proved no provider child remains', - observedAt: args.now - } + deathEvidence: { + kind: 'exit-observed', + detail: + args.exitProof === 'root-exit-observed' + ? 'the provider process exited; its descendants were not proven gone' + : 'post-acquisition cleanup proved no provider child remains', + observedAt: args.now, + ownerFence: record.lease.runtimeFence + } }) state.records.set(args.sessionId, next) state.operations = settleAgentSessionOperation(state.operations, args) @@ -133,14 +132,17 @@ function settleFailedLease( throw new Error('agent_session_ownership_unknown') } if (args.exitProof === 'unproven' && record.lease.ownerProcess) { - return withLease(record, { - ...record.lease, - handoffStage: 'recovering', - // The operation is durably settled failed below; a lease still naming it would - // read as an in-flight transfer to every consumer that keys on the stage + id pair. - handoffOperationId: null, - lastRenewedAt: args.now - }) + // Parking in recovery proves nothing alive, so `lastRenewedAt` keeps the spawn's proof. + return { + ...withLease(record, { + ...record.lease, + handoffStage: 'recovering', + // The operation is durably settled failed below; a lease still naming it would + // read as an in-flight transfer to every consumer that keys on the stage + id pair. + handoffOperationId: null + }), + updatedAt: args.now + } } return withLease(record, { ...record.lease, @@ -151,7 +153,7 @@ function settleFailedLease( claimStatus: 'released', lastRenewedAt: args.now, handoffOperationId: null, - deathEvidence: acquisitionDeathEvidence(args.exitProof, args.now) + deathEvidence: acquisitionDeathEvidence(args.exitProof, args.now, record.lease.runtimeFence) }) } @@ -159,25 +161,27 @@ function settleFailedLease( * when nothing was. */ function acquisitionDeathEvidence( exitProof: AgentSessionAcquisitionExitProof, - observedAt: number + observedAt: number, + ownerFence: number ): AgentSessionDeathEvidence | null { if (exitProof === 'unproven') { return null } + const proof = { observedAt, ownerFence } if (exitProof === 'processless') { - return { kind: 'pid-absent', detail: 'reservation failed before spawn', observedAt } + return { kind: 'pid-absent', detail: 'reservation failed before spawn', ...proof } } if (exitProof === 'root-exit-observed') { return { kind: 'exit-observed', detail: 'the provider process exited; its descendants were not proven gone', - observedAt + ...proof } } // Cleanup proved no child of this attempt remains; it may never have spawned. return { kind: 'exit-observed', detail: 'acquisition cleanup proved no provider child remains', - observedAt + ...proof } } diff --git a/src/main/runtime/agent-session-death-evidence-persistence.test.ts b/src/main/runtime/agent-session-death-evidence-persistence.test.ts new file mode 100644 index 00000000000..6e0bff8098c --- /dev/null +++ b/src/main/runtime/agent-session-death-evidence-persistence.test.ts @@ -0,0 +1,228 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { AgentSessionDeathEvidence } from '../../shared/agent-session-record' +import { + agentSessionLeaseFixture, + agentSessionRecordFixture +} from '../../shared/agent-session-record.test-fixture' +import type { AgentSessionFailedAcquisitionSettlement } from './agent-session-acquisition-failure-settlement' +import { AgentSessionRecordStore } from './agent-session-record-store' +import { agentSessionStorePath } from './agent-session-record-store-file' + +const SESSION = 'session-alpha-1' +/** The fixture lease's last renewal. */ +const LAST_RENEWED_AT = 30_000 + +let directory: string + +async function seed(deathEvidence: AgentSessionDeathEvidence | null): Promise { + const lease = + deathEvidence === null + ? agentSessionLeaseFixture() + : agentSessionLeaseFixture({ + ownerProcess: null, + reservedSpawnToken: null, + claimStatus: 'released', + deathEvidence + }) + await writeFile( + agentSessionStorePath(directory), + JSON.stringify({ + schemaVersion: 2, + hostId: 'local', + records: { [SESSION]: agentSessionRecordFixture(lease) }, + operations: {}, + retiredClaimKeys: [], + unusableRecords: {} + }) + ) +} + +function open(): Promise { + return AgentSessionRecordStore.open({ directory, hostId: 'local' }) +} + +beforeEach(async () => { + directory = await mkdtemp(join(tmpdir(), 'orca-death-evidence-')) +}) + +afterEach(async () => { + await rm(directory, { recursive: true, force: true }) +}) + +describe('death evidence on disk', () => { + it('records the last renewal before the crash and reads it back after another restart', async () => { + await seed(null) + const crashed = await open() + await crashed.reconcileOnRestart({ + probe: async () => ({ outcome: 'pid-absent' }), + now: 90_000 + }) + const evidence = { + kind: 'pid-absent', + detail: 'recorded pid absent on host', + observedAt: 90_000, + ownerFence: 7, + lastProvenAliveAt: LAST_RENEWED_AT + } + expect(crashed.getRecord(SESSION)?.lease.deathEvidence).toEqual(evidence) + expect((await open()).getRecord(SESSION)?.lease.deathEvidence).toEqual(evidence) + }) + + it('loads evidence an older build wrote without a proven-alive time', async () => { + const olderBuild = { kind: 'pid-absent' as const, detail: 'gone', observedAt: 90_000 } + await seed(olderBuild) + const store = await open() + expect(store.isSessionUnreadable(SESSION)).toBe(false) + expect(store.getRecord(SESSION)?.lease.deathEvidence).toEqual(olderBuild) + }) + + it.each([ + ['proves the owner alive after the probe found it gone', { lastProvenAliveAt: 90_001 }], + ['names no possible owner', { ownerFence: -1 }] + ])('quarantines evidence that %s', async (_why, field) => { + await seed({ kind: 'pid-absent', detail: 'gone', observedAt: 90_000, ...field }) + expect((await open()).isSessionUnreadable(SESSION)).toBe(true) + }) +}) + +describe('who is told a proof of death landed', () => { + it('tells a listener once the proof is committed, and never for a write that proves nothing', async () => { + await seed(null) + const store = await open() + const told: unknown[] = [] + store.onDeathEvidence((sessionId) => told.push(store.getRecord(sessionId)?.lease.deathEvidence)) + const unsubscribed = vi.fn() + store.onDeathEvidence(unsubscribed)() + + await expect( + store.evictProvenDeadOwner({ + sessionId: SESSION, + expectedFence: 6, + probe: { outcome: 'pid-absent' }, + now: 80_000 + }) + ).rejects.toThrow() + await store.reconcileOnRestart({ probe: async () => ({ outcome: 'pid-absent' }), now: 90_000 }) + // The proof already on the record is not news to a later write. + await store.setSessionTabVisibility(SESSION, true) + + expect(told).toEqual([expect.objectContaining({ kind: 'pid-absent', ownerFence: 7 })]) + expect(unsubscribed).not.toHaveBeenCalled() + }) +}) + +describe('a failed acquisition', () => { + const NOW = 1_800_000_000_000 + const OPERATION_ID = `${NOW}-${'1'.padStart(32, '0')}` + + /** Reserve and observe the spawn at NOW, as an attach does before it can fail. */ + async function spawnedOwner(store: AgentSessionRecordStore): Promise { + const reserved = await store.reserveOwner({ + sessionId: SESSION, + location: agentSessionRecordFixture().location, + provider: 'claude', + accountHome: { variable: 'CLAUDE_CONFIG_DIR', path: '/home/dev/.claude' }, + expectedFence: null, + spawnToken: 'spawn-a', + claimKeyId: 'key-1', + handoffOperationId: OPERATION_ID, + probe: { outcome: 'indeterminate', reason: 'no answer' }, + operation: { callerKey: 'client-1', operationId: OPERATION_ID, fingerprint: 'fp-1' }, + now: NOW + }) + const fence = reserved.record.lease.runtimeFence + await store.commitProcessIdentity({ + sessionId: SESSION, + fence, + process: { hostId: 'local', pid: 4242, processStartTimeMs: NOW, spawnToken: 'spawn-a' }, + now: NOW + }) + return fence + } + + function unproven(fence: number, now: number): AgentSessionFailedAcquisitionSettlement { + return { + sessionId: SESSION, + fence, + spawnToken: 'spawn-a', + callerKey: 'client-1', + operationId: OPERATION_ID, + outcome: { status: 'failed', code: 'agent_session_operation_invalid', message: 'failed' }, + exitProof: 'unproven', + now + } + } + + it.each([ + ['before the owner proved its handle', false], + ['after the owner proved its handle', true] + ] as const)( + 'parks in recovery keeping the last proof of life, not the failure, %s', + async (_when, proved) => { + const store = await open() + const fence = await spawnedOwner(store) + if (proved) { + await store.proveOwner({ + sessionId: SESSION, + fence, + link: { + linkId: 'link-1', + handle: { provider: 'claude', sessionId: 'provider-session-1', leafUuid: null }, + origin: 'created', + mintedAtFence: fence, + observedAt: NOW + }, + now: NOW + }) + await store.settleFailedPostAcquisitionAttachment(unproven(fence, NOW + 60_000)) + } else { + await store.settleFailedAcquisition(unproven(fence, NOW + 60_000)) + } + expect(store.getRecord(SESSION)?.lease).toMatchObject({ + handoffStage: 'recovering', + lastRenewedAt: NOW + }) + + const evicted = await store.evictProvenDeadOwner({ + sessionId: SESSION, + expectedFence: fence, + probe: { outcome: 'pid-absent' }, + now: NOW + 120_000 + }) + expect(evicted.lease.deathEvidence).toMatchObject({ lastProvenAliveAt: NOW }) + } + ) + + it.each(['exit-proven', 'root-exit-observed', 'processless'] as const)( + 'names its own fence in the proof when cleanup settles it %s', + async (exitProof) => { + const store = await open() + const fence = await spawnedOwner(store) + const settled = + exitProof === 'processless' + ? await store.settleFailedAcquisition({ ...unproven(fence, NOW), exitProof }) + : await (async () => { + await store.proveOwner({ + sessionId: SESSION, + fence, + link: { + linkId: 'link-1', + handle: { provider: 'claude', sessionId: 'provider-session-1', leafUuid: null }, + origin: 'created', + mintedAtFence: fence, + observedAt: NOW + }, + now: NOW + }) + return store.settleFailedPostAcquisitionAttachment({ + ...unproven(fence, NOW), + exitProof + }) + })() + expect(settled.lease.deathEvidence).toMatchObject({ ownerFence: fence }) + } + ) +}) diff --git a/src/main/runtime/agent-session-orphan-child-reaper.test.ts b/src/main/runtime/agent-session-orphan-child-reaper.test.ts deleted file mode 100644 index eb837ac4a22..00000000000 --- a/src/main/runtime/agent-session-orphan-child-reaper.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -// A child spawned under a reservation whose record was lost is invisible to the lease. Reaping -// is bounded cleanup only; it never replaces the lease proof required to grant another writer. - -import { describe, expect, it, vi } from 'vitest' -import type { AgentSessionRecordStore } from './agent-session-record-store' -import { stopOrphanAgentSessionChildren } from './agent-session-orphan-child-reaper' - -function storeWithLeasedTokens(tokens: readonly string[]) { - return { - listOrphanSpawnTokens: (observed: readonly string[]) => - observed.filter((token) => !tokens.includes(token)) - } as Pick -} - -describe('orphan agent-session child reaper', () => { - it('stops every process whose spawn token no lease claims', async () => { - const stop = vi.fn() - - const stopped = await stopOrphanAgentSessionChildren({ - store: storeWithLeasedTokens(['token-owned']), - scan: async () => - new Map([ - ['token-owned', [101]], - ['token-lost', [202, 203]] - ]), - stop - }) - - expect(stopped).toEqual([202, 203]) - expect(stop).toHaveBeenCalledWith(202, 'SIGTERM') - expect(stop).toHaveBeenCalledWith(203, 'SIGTERM') - expect(stop).not.toHaveBeenCalledWith(101, 'SIGTERM') - }) - - it('stops nothing on a host that cannot enumerate spawn tokens', async () => { - const stop = vi.fn() - - // Null is "cannot answer", never "no tokens" — treating it as an empty scan would be a - // license to signal nothing, but a future empty-map reading would be a license to signal - // whatever the caller guessed. - const stopped = await stopOrphanAgentSessionChildren({ - store: { - listOrphanSpawnTokens: () => { - throw new Error('the reaper must not ask when the host could not answer') - } - }, - scan: async () => null, - stop - }) - - expect(stopped).toEqual([]) - expect(stop).not.toHaveBeenCalled() - }) - - it('surfaces a failure to signal an observed orphan', async () => { - const failure = Object.assign(new Error('not permitted'), { code: 'EPERM' }) - - await expect( - stopOrphanAgentSessionChildren({ - store: storeWithLeasedTokens([]), - scan: async () => new Map([['token-lost', [202]]]), - stop: () => { - throw failure - } - }) - ).rejects.toBe(failure) - }) -}) diff --git a/src/main/runtime/agent-session-orphan-child-reaper.ts b/src/main/runtime/agent-session-orphan-child-reaper.ts deleted file mode 100644 index 68d9c899024..00000000000 --- a/src/main/runtime/agent-session-orphan-child-reaper.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Best-effort stop for provider children that carry an Orca spawn token no lease claims. - * - * A child spawned under a reservation whose record was lost — the primary store file went with it, - * or the crash beat the durable write — is unreachable but still connected to the provider session. - * Only a token match justifies signalling a process; neither age nor CPU is evidence, and a host - * that cannot enumerate tokens stops nothing. This never proves process exit or licenses a new - * owner; lease adjudication remains the single-writer boundary. - */ - -import type { AgentSessionRecordStore } from './agent-session-record-store' -import { - scanAgentSessionSpawnTokenProcesses, - type AgentSessionSpawnTokenScan -} from './agent-session-spawn-token-process-scan' - -export type AgentSessionOrphanStopSignal = 'SIGTERM' | 'SIGKILL' - -function defaultStop(pid: number, signal: AgentSessionOrphanStopSignal): void { - try { - process.kill(pid, signal) - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ESRCH') { - throw error - } - } -} - -/** Returns the pids signalled, so the caller can report what it reaped. */ -export async function stopOrphanAgentSessionChildren(input: { - store: Pick - scan?: () => Promise - stop?: (pid: number, signal: AgentSessionOrphanStopSignal) => void -}): Promise { - const observed = await (input.scan ?? scanAgentSessionSpawnTokenProcesses)() - if (observed === null || observed.size === 0) { - return [] - } - const stop = input.stop ?? defaultStop - const stopped: number[] = [] - for (const token of input.store.listOrphanSpawnTokens([...observed.keys()])) { - for (const pid of observed.get(token) ?? []) { - stop(pid, 'SIGTERM') - stopped.push(pid) - } - } - return stopped -} diff --git a/src/main/runtime/agent-session-record-store.test.ts b/src/main/runtime/agent-session-record-store.test.ts index ce38e43d0ea..5ee25d36813 100644 --- a/src/main/runtime/agent-session-record-store.test.ts +++ b/src/main/runtime/agent-session-record-store.test.ts @@ -753,13 +753,7 @@ describe('host and workspace isolation', () => { }) }) -describe('orphans, claim keys, checkpoints, and unreadable rows', () => { - it('calls a spawn token with no lease an orphan', async () => { - const store = await open() - await establishOwner(store) - expect(store.listOrphanSpawnTokens(['spawn-a', 'spawn-z'])).toEqual(['spawn-z']) - }) - +describe('claim keys, checkpoints, and unreadable rows', () => { it('keeps a retired claim key verifiable for the retention window', async () => { const store = await open() await store.retireClaimKey('key-1', NOW) diff --git a/src/main/runtime/agent-session-record-store.ts b/src/main/runtime/agent-session-record-store.ts index 7df509dd810..925fa38d8d1 100644 --- a/src/main/runtime/agent-session-record-store.ts +++ b/src/main/runtime/agent-session-record-store.ts @@ -25,7 +25,6 @@ import { retireAgentSessionClaimKey } from './agent-session-claim-key-retention' import type { AgentSessionOwnerProbe } from '../../shared/agent-session-lease-adjudication' -import { classifyObservedAgentSessionSpawnToken } from '../../shared/agent-session-lease-adjudication' import type { AgentSessionProviderHandleLink } from '../../shared/agent-session-provider-handle' import { agentSessionScopeKey, @@ -78,6 +77,8 @@ export const AGENT_SESSION_LEASE_TTL_MS = 30_000, AGENT_SESSION_LEASE_RENEW_INTERVAL_MS = 10_000 export class AgentSessionRecordStore { + private readonly deathEvidenceListeners = new Set<(sessionId: string) => void>() + private constructor(private readonly transactions: AgentSessionStoreTransactionQueue) {} static async open(args: { directory: string; hostId: string }): Promise { @@ -180,14 +181,6 @@ export class AgentSessionRecordStore { isClaimKeyVerifiable = (keyId: string, now: number): boolean => isAgentSessionClaimKeyVerifiable(this.state, keyId, now) - /** Spawn tokens observed on the host with no matching lease. Stop them; never adopt them. */ - listOrphanSpawnTokens(observedTokens: readonly string[]): string[] { - const leases = this.listRecords().map((record) => record.lease) - return observedTokens.filter( - (spawnToken) => classifyObservedAgentSessionSpawnToken({ spawnToken, leases }) === 'orphan' - ) - } - async reserveOwner(request: AgentSessionReserveRequest): Promise { return this.transact(() => commitAgentSessionReservation(this.state, request, AGENT_SESSION_LEASE_TTL_MS) @@ -337,6 +330,32 @@ export class AgentSessionRecordStore { }) } + /** Told, once committed, of each session a transaction wrote a proof of death for — whichever + * transition wrote it, since every one lands here. Must not throw. */ + onDeathEvidence(listener: (sessionId: string) => void): () => void { + this.deathEvidenceListeners.add(listener) + return () => this.deathEvidenceListeners.delete(listener) + } + /** Serialize every mutation against the latest committed disk state. */ - private transact = (apply: () => T): Promise => this.transactions.transact(apply) + private transact = async (apply: () => T): Promise => { + let proven: string[] = [] + const result = await this.transactions.transact(() => { + if (this.deathEvidenceListeners.size === 0) { + return apply() + } + const before = new Map( + [...this.state.records].map(([id, record]) => [id, record.lease.deathEvidence]) + ) + const applied = apply() + proven = [...this.state.records] + .filter(([id, { lease }]) => lease.deathEvidence && lease.deathEvidence !== before.get(id)) + .map(([id]) => id) + return applied + }) + for (const sessionId of proven) { + this.deathEvidenceListeners.forEach((listener) => listener(sessionId)) + } + return result + } } diff --git a/src/main/runtime/agent-session-restart-lease-transitions.ts b/src/main/runtime/agent-session-restart-lease-transitions.ts index 70104588a58..b4269d21c67 100644 --- a/src/main/runtime/agent-session-restart-lease-transitions.ts +++ b/src/main/runtime/agent-session-restart-lease-transitions.ts @@ -52,10 +52,13 @@ export function applyAgentSessionRestartAdjudication(args: { deathEvidence: adjudication.evidence }) } - return withLease(record, { - ...record.lease, - handoffStage: adjudication.stage, - unreconciled: false, - lastRenewedAt: args.now - }) + // Parking in recovery proves nothing alive, so `lastRenewedAt` keeps the pre-crash proof. + return { + ...withLease(record, { + ...record.lease, + handoffStage: adjudication.stage, + unreconciled: false + }), + updatedAt: args.now + } } diff --git a/src/main/runtime/agent-session-spawn-token-process-scan.ts b/src/main/runtime/agent-session-spawn-token-process-scan.ts index 6db1c069110..55e860f9320 100644 --- a/src/main/runtime/agent-session-spawn-token-process-scan.ts +++ b/src/main/runtime/agent-session-spawn-token-process-scan.ts @@ -56,7 +56,7 @@ export async function scanAgentSessionSpawnTokenProcesses( /** * Diagnostic evidence only. A null result is deliberately typed as * `unverifiable`, not as an empty process set; callers must never use this - * Linux read-back as ownership or orphan-reaping proof. + * Linux read-back as ownership proof. */ export async function scanAgentSessionSpawnTokenEvidence( platform: NodeJS.Platform = process.platform, diff --git a/src/main/runtime/agent-session-surface-release-transition.test.ts b/src/main/runtime/agent-session-surface-release-transition.test.ts index 5e671acc77f..0ea30d90197 100644 --- a/src/main/runtime/agent-session-surface-release-transition.test.ts +++ b/src/main/runtime/agent-session-surface-release-transition.test.ts @@ -18,5 +18,7 @@ describe('agent session surface release transition', () => { }) expect(released.lease.runtimeFence).toBe(9) + // The proof names the owner it released, not the fence the floor moved to. + expect(released.lease.deathEvidence).toMatchObject({ kind: 'exit-observed', ownerFence: 7 }) }) }) diff --git a/src/main/runtime/agent-session-surface-release-transition.ts b/src/main/runtime/agent-session-surface-release-transition.ts index 7da1e7633ef..fe98a870524 100644 --- a/src/main/runtime/agent-session-surface-release-transition.ts +++ b/src/main/runtime/agent-session-surface-release-transition.ts @@ -51,7 +51,8 @@ export function releaseAgentSessionOwnerAfterSurfaceClose(args: { deathEvidence: { kind: 'exit-observed', detail: args.exitReason ?? 'the last surface holding this session released it', - observedAt: args.exitObservedAt ?? args.now + observedAt: args.exitObservedAt ?? args.now, + ownerFence: record.lease.runtimeFence } }) } diff --git a/src/main/runtime/agent-transcript-replay-test-harness.ts b/src/main/runtime/agent-transcript-replay-test-harness.ts new file mode 100644 index 00000000000..5b57656ab4a --- /dev/null +++ b/src/main/runtime/agent-transcript-replay-test-harness.ts @@ -0,0 +1,56 @@ +// Replays captured PTY bytes the way onPtyData does, for suites asserting a rule on every frame. +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { HeadlessEmulator } from '../daemon/headless-emulator' +import { projectTerminalVisibleLines } from './orca-runtime-terminal-projection' +import { normalizeTerminalChunk } from './terminal-ansi-normalization' +import { appendNormalizedToTailBuffer } from './terminal-tail-buffer' +import { buildPreview } from './terminal-tail-state' +import { buildTerminalWaitText } from './terminal-wait-tail-state' + +const DEFAULT_CHUNK_CHARS = 64 + +export type TranscriptReplayFrame = { screenLines: string[]; waitText: string } + +export function readRuntimeFixture(name: string): string { + return readFileSync(join(__dirname, '__fixtures__', `${name}.txt`), 'utf8') +} + +/** A string is cut into fixed 64-char chunks; an array replays the recorded PTY chunks as-is. */ +export async function* replayTranscript( + data: string | readonly string[], + cols: number, + rows: number +): AsyncGenerator { + const chunks = typeof data === 'string' ? splitIntoChunks(data) : data + const emulator = new HeadlessEmulator({ cols, rows }) + let lines: string[] = [] + let partialLine = '' + let pendingAnsi = '' + let redrawCursor: ReturnType['redrawCursor'] = null + try { + for (const chunk of chunks) { + await emulator.write(chunk) + const normalized = normalizeTerminalChunk(chunk, pendingAnsi) + pendingAnsi = normalized.pendingAnsi + const tail = appendNormalizedToTailBuffer(lines, partialLine, normalized.text, redrawCursor) + lines = tail.lines + partialLine = tail.partialLine + redrawCursor = tail.redrawCursor + yield { + screenLines: projectTerminalVisibleLines(emulator).lines, + waitText: buildTerminalWaitText(lines, partialLine, buildPreview(lines, partialLine)) + } + } + } finally { + emulator.dispose() + } +} + +function splitIntoChunks(data: string): string[] { + const chunks: string[] = [] + for (let offset = 0; offset < data.length; offset += DEFAULT_CHUNK_CHARS) { + chunks.push(data.slice(offset, offset + DEFAULT_CHUNK_CHARS)) + } + return chunks +} diff --git a/src/main/runtime/claude-trust-dialog-transcript.test.ts b/src/main/runtime/claude-trust-dialog-transcript.test.ts index d459fc51502..5913c54b019 100644 --- a/src/main/runtime/claude-trust-dialog-transcript.test.ts +++ b/src/main/runtime/claude-trust-dialog-transcript.test.ts @@ -31,6 +31,8 @@ async function waitOnReplay( size, data: '' }) + // Pane creation awaits real timers; replay and polling use the virtual clock. + vi.useFakeTimers() const bytes = Buffer.from(data, 'utf8') const step = readSize ?? bytes.length // Why a streaming decoder: a PTY read can end inside a multi-byte character, as a real one does. @@ -42,14 +44,22 @@ async function waitOnReplay( if (options.after) { runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, options.after, Date.now()) } - // Why a wide budget: a blocked verdict lands on the first ~2 s poll tick; the slack absorbs load. - return runtime.waitForTerminal(handle, { + const timeoutMs = options.timeoutMs ?? 15_000 + const promise = runtime.waitForTerminal(handle, { condition: 'tui-idle', - timeoutMs: options.timeoutMs ?? 15_000 + timeoutMs }) + // Attach before advancing so the expected timeout is never an unhandled rejection. + promise.catch(() => {}) + await vi.advanceTimersByTimeAsync(timeoutMs) + return promise } describe("Claude's workspace trust dialog, from captured transcripts", () => { + afterEach(() => { + vi.useRealTimers() + }) + it('reports the dialog as a blocking prompt instead of waiting out the whole budget', async () => { await expect(waitOnReplay('claude-dialog-trust-workspace', null)).resolves.toMatchObject({ satisfied: false, diff --git a/src/main/runtime/codex-header-readiness-transcripts.test.ts b/src/main/runtime/codex-header-readiness-transcripts.test.ts index 6cb3c08179f..0cfcf548017 100644 --- a/src/main/runtime/codex-header-readiness-transcripts.test.ts +++ b/src/main/runtime/codex-header-readiness-transcripts.test.ts @@ -1,14 +1,11 @@ -import { readFileSync } from 'node:fs' -import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' -import { HeadlessEmulator } from '../daemon/headless-emulator' import { createTranscriptPane } from './agent-transcript-pane-test-harness' -import { projectTerminalVisibleLines } from './orca-runtime-terminal-projection' -import { normalizeTerminalChunk } from './terminal-ansi-normalization' -import { appendNormalizedToTailBuffer } from './terminal-tail-buffer' -import { buildPreview } from './terminal-tail-state' +import { + readRuntimeFixture, + replayTranscript, + type TranscriptReplayFrame +} from './agent-transcript-replay-test-harness' import { isKnownReadyPromptBody, isKnownReadyPromptPreview } from './terminal-wait-detection' -import { buildTerminalWaitText } from './terminal-wait-tail-state' vi.mock('electron', () => ({ BrowserWindow: { fromId: vi.fn(() => null) }, @@ -23,44 +20,14 @@ const EFFORT_OVERRIDE = 'codex-0157-effort-override-embedded-warning' const CONFIG_OVERRIDE = 'codex-0157-config-override-embedded-warning' const NO_DAEMON = 'codex-0157-no-daemon-effort-override' const ALL_FIXTURES = [PLAIN, EFFORT_OVERRIDE, CONFIG_OVERRIDE, NO_DAEMON] -const CHUNK_CHARS = 64 -function readFixture(name: string): string { - return readFileSync(join(__dirname, '__fixtures__', `${name}.txt`), 'utf8') -} - -type ReplayFrame = { screenLines: string[]; waitText: string } - -/** Feeds the bytes the way onPtyData does: one emulator grid, one line-folded wait text. */ -async function* replay(data: string, cols: number, rows: number): AsyncGenerator { - const emulator = new HeadlessEmulator({ cols, rows }) - let lines: string[] = [] - let partialLine = '' - let pendingAnsi = '' - let redrawCursor: ReturnType['redrawCursor'] = null - try { - for (let offset = 0; offset < data.length; offset += CHUNK_CHARS) { - const chunk = data.slice(offset, offset + CHUNK_CHARS) - await emulator.write(chunk) - const normalized = normalizeTerminalChunk(chunk, pendingAnsi) - pendingAnsi = normalized.pendingAnsi - const tail = appendNormalizedToTailBuffer(lines, partialLine, normalized.text, redrawCursor) - lines = tail.lines - partialLine = tail.partialLine - redrawCursor = tail.redrawCursor - yield { - screenLines: projectTerminalVisibleLines(emulator).lines, - waitText: buildTerminalWaitText(lines, partialLine, buildPreview(lines, partialLine)) - } - } - } finally { - emulator.dispose() - } -} - -async function finalFrame(name: string, cols: number, rows: number): Promise { - let last: ReplayFrame | null = null - for await (const frame of replay(readFixture(name), cols, rows)) { +async function finalFrame( + name: string, + cols: number, + rows: number +): Promise { + let last: TranscriptReplayFrame | null = null + for await (const frame of replayTranscript(readRuntimeFixture(name), cols, rows)) { last = frame } if (!last) { @@ -89,8 +56,8 @@ describe('Codex 0.157 header readiness from captured bytes', () => { '%s: the screen never adds readiness while loading, and is ready at the final screen', async (name) => { let sawLoadingHeader = false - let last: ReplayFrame | null = null - for await (const frame of replay(readFixture(name), 120, 40)) { + let last: TranscriptReplayFrame | null = null + for await (const frame of replayTranscript(readRuntimeFixture(name), 120, 40)) { if (screenShowsLoadingHeader(frame.screenLines)) { sawLoadingHeader = true expect(isKnownReadyPromptBody('', 'codex', () => frame.screenLines)).toBe(false) @@ -113,7 +80,7 @@ describe('Codex 0.157 header readiness from captured bytes', () => { [60, 5] ])('at %ix%i the screen never takes readiness away from the text rules', (cols, rows) => { it.each(ALL_FIXTURES)('%s', async (name) => { - for await (const frame of replay(readFixture(name), cols, rows)) { + for await (const frame of replayTranscript(readRuntimeFixture(name), cols, rows)) { if (isKnownReadyPromptPreview(frame.waitText)) { expect(isKnownReadyPromptBody(frame.waitText, 'codex', () => frame.screenLines)).toBe( true @@ -191,7 +158,7 @@ describe('Codex 0.157 header readiness from captured bytes', () => { paneTitle: 'Terminal', foregroundProcess: 'codex', launchAgent: 'codex', - data: readFixture(name), + data: readRuntimeFixture(name), size }) } diff --git a/src/main/runtime/codex-quiet-ready-screen.test.ts b/src/main/runtime/codex-quiet-ready-screen.test.ts new file mode 100644 index 00000000000..50052365905 --- /dev/null +++ b/src/main/runtime/codex-quiet-ready-screen.test.ts @@ -0,0 +1,372 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { TuiAgent } from '../../shared/tui-agent' +import { createTranscriptPane } from './agent-transcript-pane-test-harness' +import { + readRuntimeFixture, + replayTranscript, + type TranscriptReplayFrame +} from './agent-transcript-replay-test-harness' +import { isCodexComposerReadyScreen } from './codex-terminal-readiness' +import { + detectTerminalWaitBlockedReason, + isKnownReadyPromptBody, + isQuietReadyScreenBody +} from './terminal-wait-detection' +import { + evaluateTuiIdle, + hasQuietReadyScreen, + isTuiIdleReadyVerdict, + type TuiIdleEvaluationInput +} from './tui-idle-evidence' + +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 QUIESCENCE_MS = 3000 + +// codex-cli recordings at 120x40 (see each .meta.json); STA-8834. Each launches, runs a turn, +// and ends idle; the timed ones also replay as plain strings here. 0.157 idles via STA-8628's. +const TIMED_FIXTURES = ['codex-0-155-1-timed-turn', 'codex-0-158-0-timed-turn'] +const SETTLED_IDLE_FIXTURES = ['codex-0-150-1-turn', ...TIMED_FIXTURES] +const DIALOG_FIXTURES = [ + 'codex-0-157-1-update-dialog', + 'codex-0-158-0-approval', + 'codex-0-158-0-trustprompt' +] +const STA_8628_FIXTURES = [ + 'codex-0157-plain-ready', + 'codex-0157-effort-override-embedded-warning', + 'codex-0157-config-override-embedded-warning', + 'codex-0157-no-daemon-effort-override' +] + +const BUSY_STATUS_RE = /to interrupt\)/ +const EMPTY_COMPOSER_RE = /^› ask codex to do anything\s*$/m +const HEADER_LOADING_RE = /(?:model|directory):\s+loading|^\s*loading\s*$/m +const DIALOG_RE = + /update available|do you trust|trust this folder|would you like to run|press enter to confirm|enter continue/ + +function screenOf(frame: TranscriptReplayFrame): string { + return frame.screenLines.join('\n').toLowerCase() +} + +async function collectFrames( + data: string | readonly string[], + cols = 120, + rows = 40 +): Promise { + const frames: TranscriptReplayFrame[] = [] + for await (const frame of replayTranscript(data, cols, rows)) { + frames.push(frame) + } + return frames +} + +describe('Codex composer ready screen, frame by frame', () => { + it.each([...SETTLED_IDLE_FIXTURES, ...DIALOG_FIXTURES])( + '%s: never ready while loading, mid-turn, or under a drawn dialog', + async (name) => { + const seen = { loading: 0, busy: 0, dialog: 0 } + for (const frame of await collectFrames(readRuntimeFixture(name))) { + const screen = screenOf(frame) + // Why only once the composer is gone: a half-drawn dialog keeps it for a few frames, + // which quiescence absorbs; a drawn dialog replaces it. + const kind = HEADER_LOADING_RE.test(screen) + ? 'loading' + : BUSY_STATUS_RE.test(screen) + ? 'busy' + : DIALOG_RE.test(screen) && !EMPTY_COMPOSER_RE.test(screen) + ? 'dialog' + : null + if (kind) { + seen[kind] += 1 + expect({ kind, ready: isCodexComposerReadyScreen(screenOf(frame)) }).toEqual({ + kind, + ready: false + }) + } + } + // Presence preconditions: each fixture exercises the states it was recorded for. + expect(seen.loading).toBeGreaterThan(0) + if (DIALOG_FIXTURES.includes(name)) { + expect(seen.dialog).toBeGreaterThan(0) + } else { + expect(seen.busy).toBeGreaterThan(0) + } + }, + 30_000 + ) + + it.each([...SETTLED_IDLE_FIXTURES, ...STA_8628_FIXTURES])( + '%s: ready on the final idle screen', + async (name) => { + const frames = await collectFrames(readRuntimeFixture(name)) + expect(isCodexComposerReadyScreen(screenOf(frames.at(-1)!))).toBe(true) + } + ) + + it.each(DIALOG_FIXTURES)('%s: not ready while the dialog owns the screen', async (name) => { + const frames = await collectFrames(readRuntimeFixture(name)) + expect(isCodexComposerReadyScreen(screenOf(frames.at(-1)!))).toBe(false) + }) + + it('is not ready without a live screen', () => { + expect(isQuietReadyScreenBody('', 'codex', () => null)).toBe(false) + }) + + it('reads the placeholder only as the composer line, not quoted mid-line', () => { + const quoted = ['$ grep placeholder notes.txt', 'the box says › Ask Codex to do anything', '$'] + expect(isQuietReadyScreenBody('', 'codex', () => quoted)).toBe(false) + }) + + it('reads the busy row only above the composer, not a status row quoted in the answer', () => { + const screenLines = [ + '>_ OpenAI Codex (v0.158.0)', + ' ~/repo', + '', + '› what does the status row look like mid-turn?', + '', + '• It reads like this:', + '', + ' • Working (0s • esc to interrupt)', + '', + ' The timer counts up until the turn ends, and the row', + ' disappears once the answer is complete.', + '', + '› Ask Codex to do anything', + '', + ' gpt-5.6-sol medium · ~/repo', + ' ? for shortcuts' + ] + expect(isQuietReadyScreenBody('', 'codex', () => screenLines)).toBe(true) + const composer = screenLines.indexOf('› Ask Codex to do anything') + const busy = [ + ...screenLines.slice(0, composer), + '• Working (3s • esc to interrupt)', + ' └ Tip: start a fresh idea with /new', + '', + ...screenLines.slice(composer) + ] + expect(isQuietReadyScreenBody('', 'codex', () => busy)).toBe(false) + }) + + it("reads 0.158's turn timestamp above the composer as the end of a quoted status row", () => { + // Live 0.158 screen after a worker answered with a status row verbatim (STA-8834). + const screenLines = [ + ' === TASK ===', + ' Reply with exactly this line and nothing else: • Working (0s • esc to interrupt) — then call worker_done.', + '• • Working (0s • esc to interrupt)', + '• Ran orca-dev orchestration send --from term_f496fcd2-a874-4fa6-a9bd-550a515ac928 --dispatch-capability dcap_PTgPpvf-B…', + ' └ Sent msg_ff7be3f25d7e', + ' + Show details', + '• • Working (0s • esc to interrupt)', + ' 11:15 PM', + '› Ask Codex to do anything', + ' GPT-6-Sol medium · ~/orca-lanes/sta8834/live3/scratch · Report task outcome', + ' ? for shortcuts' + ] + expect(isQuietReadyScreenBody('', 'codex', () => screenLines)).toBe(true) + }) + + it('settles a quiet composer under an answer asking "Would you like to proceed?"', () => { + const screenLines = [ + '>_ OpenAI Codex (v0.158.0)', + ' ~/repo', + '', + '› tidy the README', + '', + '• Done. Would you like to proceed with the changelog too?', + '', + '› Ask Codex to do anything', + '', + ' ? for shortcuts' + ] + const waitText = screenLines.join('\n') + const verdict = evaluateTuiIdle({ + record: { lastAgentStatus: null, lastOutputAt: 0, lastOscTitle: null }, + readTailBlockedReason: () => detectTerminalWaitBlockedReason(waitText), + readPositiveBodyEvidence: () => isKnownReadyPromptBody(waitText, 'codex', () => screenLines), + readQuietReadyBodyEvidence: () => + isQuietReadyScreenBody(waitText, 'codex', () => screenLines), + agent: 'codex', + firstPartyStatus: null, + quiescenceMs: QUIESCENCE_MS + }) + expect(verdict.kind).toBe('ready-strong') + }) +}) + +type Timing = { promptSentAtMs: number; chunks: [number, number][] } + +function readTimedFixture(name: string): { chunks: string[]; times: number[]; promptAt: number } { + const data = readRuntimeFixture(name) + const timing: Timing = JSON.parse( + readFileSync(join(__dirname, '__fixtures__', `${name}.timing.json`), 'utf8') + ) + const chunks: string[] = [] + let offset = 0 + for (const [, length] of timing.chunks) { + chunks.push(data.slice(offset, offset + length)) + offset += length + } + expect(offset).toBe(data.length) + return { chunks, times: timing.chunks.map(([at]) => at), promptAt: timing.promptSentAtMs } +} + +describe('the quiet lane over recorded chunk timing (default animations)', () => { + afterEach(() => { + vi.useRealTimers() + }) + + it.each(TIMED_FIXTURES)( + '%s: never settles mid-turn, and settles once the finished turn is quiet', + async (name) => { + const { chunks, times, promptAt } = readTimedFixture(name) + const frames = await collectFrames(chunks) + // Why after the replay: the emulator's write flush runs on real timers. + vi.useFakeTimers() + const lastBusy = frames.findLastIndex((frame) => BUSY_STATUS_RE.test(screenOf(frame))) + const firstTurnChunk = times.findIndex((at) => at >= promptAt) + expect(lastBusy).toBeGreaterThan(firstTurnChunk) + // The latest moment each frame stays on screen: just before the next chunk lands. + const settlesAt = (index: number, now: number): boolean => { + vi.setSystemTime(now) + return hasQuietReadyScreen( + { lastAgentStatus: null, lastOutputAt: times[index]!, lastOscTitle: null }, + 'codex', + () => + isQuietReadyScreenBody( + frames[index]!.waitText, + 'codex', + () => frames[index]!.screenLines + ), + QUIESCENCE_MS + ) + } + let readyBodyMidTurn = 0 + for (let index = firstTurnChunk; index <= lastBusy; index += 1) { + if (isCodexComposerReadyScreen(screenOf(frames[index]!))) { + readyBodyMidTurn += 1 + } + expect({ index, settles: settlesAt(index, times[index + 1]! - 1) }).toEqual({ + index, + settles: false + }) + } + // Presence precondition: the body alone does show mid-turn, so quiescence is load-bearing. + expect(readyBodyMidTurn).toBeGreaterThan(0) + const last = frames.length - 1 + // Why +1: the fake clock keeps whole milliseconds; the recorded times do not. + expect(settlesAt(last, times[last]! + QUIESCENCE_MS + 1)).toBe(true) + }, + 120_000 + ) +}) + +describe('never less ready than origin/main', () => { + const records = [ + { lastAgentStatus: null, lastOutputAt: 0, lastOscTitle: null }, + { lastAgentStatus: 'idle' as const, lastOutputAt: 0, lastOscTitle: 'codex' }, + { lastAgentStatus: 'working' as const, lastOutputAt: 0, lastOscTitle: '⠋ repo' } + ] + describe.each([ + [120, 40], + [80, 24] + ])('at %ix%i', (cols, rows) => { + it.each([...SETTLED_IDLE_FIXTURES, ...DIALOG_FIXTURES, ...STA_8628_FIXTURES])( + '%s', + async (name) => { + for (const frame of await collectFrames(readRuntimeFixture(name), cols, rows)) { + for (const agent of ['codex', null] as const) { + const base = { + readTailBlockedReason: () => detectTerminalWaitBlockedReason(frame.waitText), + readPositiveBodyEvidence: () => + isKnownReadyPromptBody(frame.waitText, agent, () => frame.screenLines), + agent, + firstPartyStatus: null, + quiescenceMs: QUIESCENCE_MS + } satisfies Omit + for (const record of records) { + const before = evaluateTuiIdle({ + ...base, + record, + // Why a withheld screen: that leaves exactly main's Muse lane, the only widening. + readQuietReadyBodyEvidence: () => + isQuietReadyScreenBody(frame.waitText, agent, () => null) + }) + const after = evaluateTuiIdle({ + ...base, + record, + readQuietReadyBodyEvidence: () => + isQuietReadyScreenBody(frame.waitText, agent, () => frame.screenLines) + }) + if (isTuiIdleReadyVerdict(before)) { + expect(isTuiIdleReadyVerdict(after)).toBe(true) + } + } + } + } + }, + 60_000 + ) + }) +}) + +describe('agent gate', () => { + const placeholderScreen = ['› Ask Codex to do anything', ' ? for shortcuts'] + + it("never reads another agent's screen, even one showing Codex's placeholder", () => { + const readScreenLines = vi.fn(() => placeholderScreen) + for (const agent of ['claude', 'muse', 'gemini'] as const) { + expect(isQuietReadyScreenBody('', agent, readScreenLines)).toBe(false) + } + expect(readScreenLines).not.toHaveBeenCalled() + expect(isQuietReadyScreenBody('', 'codex', readScreenLines)).toBe(true) + }) + + it('leaves an agent-unknown pane showing a cat-ed Codex transcript pending', () => { + const catted = ['$ cat session.log', '› Ask Codex to do anything', ' ? for shortcuts', '$ '] + const readScreenLines = vi.fn(() => catted) + expect(isQuietReadyScreenBody(catted.join('\n'), null, readScreenLines)).toBe(false) + expect(readScreenLines).not.toHaveBeenCalled() + expect(isQuietReadyScreenBody('', 'codex', readScreenLines)).toBe(true) + }) +}) + +describe('through the runtime', () => { + // Why 0.158 alone: its header carries no `model:`, so only this lane settles it. + const CODEX_0158 = 'codex-0-158-0-timed-turn' + async function pane(name: string, launchAgent: TuiAgent, size?: { cols: number; rows: number }) { + return createTranscriptPane({ + paneTitle: 'Terminal', + foregroundProcess: launchAgent, + launchAgent, + data: readRuntimeFixture(name), + size + }) + } + + // Why 8s: quiescence (3s) plus the 2s poll re-reading the grid. + it('codex 0.158: a tui-idle wait settles once the composer is quiet', async () => { + const { runtime, handle } = await pane(CODEX_0158, 'codex', { cols: 120, rows: 40 }) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 8_000 }) + ).resolves.toMatchObject({ condition: 'tui-idle', satisfied: true }) + }, 15_000) + + it('keeps a Claude pane showing the same screen pending', async () => { + const { runtime, handle } = await pane(CODEX_0158, 'claude', { + cols: 120, + rows: 40 + }) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 6_000 }) + ).rejects.toThrow(/timeout/) + }, 15_000) +}) diff --git a/src/main/runtime/codex-terminal-readiness.ts b/src/main/runtime/codex-terminal-readiness.ts new file mode 100644 index 00000000000..3b15c3359df --- /dev/null +++ b/src/main/runtime/codex-terminal-readiness.ts @@ -0,0 +1,63 @@ +// Why both shapes: 0.150-0.157 paint `model: loading` in a box, 0.158 a bare `loading` under the title. +const CODEX_HEADER_LOADING_RE = /(?:model|directory):\s+loading|^\s*loading\s*$/m +// Why a line cap: 0.158 draws no box, so nothing else ends its header before the chat. +const CODEX_HEADER_LINES = 6 +// Why the whole line: a pager, a `cat`ed transcript, or chat can quote the placeholder mid-line. +const CODEX_EMPTY_COMPOSER_RE = /^› ask codex to do anything\s*$/ +// Why not "working": reasoning summaries replace it, and a remapped key still ends this way. +const CODEX_BUSY_STATUS_MARKER = 'to interrupt)' +// Why only a tip: it is the one line Codex draws between its status row and the composer (120x40 corpus). +const CODEX_STATUS_TIP_PREFIX = '└ tip:' + +// Why the header only: chat below it can mention "OpenAI Codex" or `model: loading`. +function findCodexHeader(screen: string): { index: number; text: string } | null { + const index = screen.indexOf('openai codex') + if (index === -1) { + return null + } + const boxEnd = screen.indexOf('╰', index) + const text = screen + .slice(index, boxEnd === -1 ? undefined : boxEnd) + .split('\n', CODEX_HEADER_LINES) + .join('\n') + return { index, text } +} + +/** Tier 1: the 0.150-0.157 header, which only a grid reassembles (see isKnownReadyPromptBody). */ +export function findCodexScreenReadyPromptIndex(screen: string): number | null { + const header = findCodexHeader(screen) + return header !== null && + header.text.includes('model:') && + header.text.includes('directory:') && + !CODEX_HEADER_LOADING_RE.test(header.text) + ? header.index + : null +} + +/** + * Tier 1b, codex panes only: the empty composer with no busy status row just above it and no + * header load. Codex 0.158 dropped `model:`/`directory:`, and a long session scrolls the header + * away, so this is its only version-stable rest body. No dialog check: every Codex dialog + * replaces the composer, while an answer ending "Would you like to…?" must not block the lane. + * The mid-turn guard is the caller's quiescence, fed by the ~100 ms title spinner and status + * timer; `tui.animations=false` (set by a screen reader), `tui.effects.progress=false`, or a + * `tui.terminal_title` without activity/spinner removes it. + */ +export function isCodexComposerReadyScreen(screen: string): boolean { + const lines = screen.split('\n') + const composer = lines.findLastIndex((line) => CODEX_EMPTY_COMPOSER_RE.test(line)) + if (composer === -1 || hasBusyStatusRowAbove(lines, composer)) { + return false + } + const header = findCodexHeader(screen) + return header === null || !CODEX_HEADER_LOADING_RE.test(header.text) +} + +// Why only the row above the composer: a finished answer (or one above 0.158's timestamp) can quote it. +function hasBusyStatusRowAbove(lines: readonly string[], composer: number): boolean { + const above = lines.slice(0, composer).filter((line) => line.trim() !== '') + const row = above.at(-1)?.trimStart().startsWith(CODEX_STATUS_TIP_PREFIX) + ? above.at(-2) + : above.at(-1) + return row?.includes(CODEX_BUSY_STATUS_MARKER) ?? false +} diff --git a/src/main/runtime/orca-runtime-activate-managed-worktree.ts b/src/main/runtime/orca-runtime-activate-managed-worktree.ts index 48d81fc79cc..28ee6cc0a92 100644 --- a/src/main/runtime/orca-runtime-activate-managed-worktree.ts +++ b/src/main/runtime/orca-runtime-activate-managed-worktree.ts @@ -118,7 +118,8 @@ export class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntimeWithListM protected async buildStartupForDraft( repo: Repo, draft: string, - requestedAgent?: TuiAgent + requestedAgent?: TuiAgent, + launchSource?: string ): Promise<{ agent: TuiAgent startup: WorktreeStartupLaunch @@ -131,6 +132,7 @@ export class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntimeWithListM repo, draft, ...(requestedAgent ? { requestedAgent } : {}), + ...(launchSource ? { launchSource } : {}), settings: this.store.getSettings(), getLaunchPlatform: () => this.getAgentLaunchPlatformForRepo(repo) }) diff --git a/src/main/runtime/orca-runtime-apply-tracked-pty-title.ts b/src/main/runtime/orca-runtime-apply-tracked-pty-title.ts index f1ea159f4a8..f4f8826e6e7 100644 --- a/src/main/runtime/orca-runtime-apply-tracked-pty-title.ts +++ b/src/main/runtime/orca-runtime-apply-tracked-pty-title.ts @@ -191,6 +191,7 @@ export class OrcaRuntimeWithApplyTrackedPtyTitle extends OrcaRuntimeWithGetUnper pty.managementTitleAt = null pty.waitBlockedAt = null pty.tailWaitState = undefined + pty.commandPaint = undefined } for (const leaf of this.getLeavesForPty(ptyId)) { leaf.lastOscTitle = null diff --git a/src/main/runtime/orca-runtime-create-agent-session.ts b/src/main/runtime/orca-runtime-create-agent-session.ts index 6c5d11b8349..61d80e60b09 100644 --- a/src/main/runtime/orca-runtime-create-agent-session.ts +++ b/src/main/runtime/orca-runtime-create-agent-session.ts @@ -27,6 +27,7 @@ import { deterministicAgentSessionUuid, isAgentSessionOperationOutcomeUnknown } from './runtime-agent-launch-resolution' +import { agentStartedTelemetry } from '../agent-launch/agent-started-telemetry' export class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSessionExecutionNamespace { async createAgentSession( @@ -201,6 +202,8 @@ export class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSe launchAgent: request.agent, terminalKittyKeyboardProtocol: request.terminalKittyKeyboardProtocol, startupCommandDelivery: startup.startupCommandDelivery, + // A fresh agent this host built; the request has no surface field, so it counts as `unknown`. + telemetry: agentStartedTelemetry(request.agent, undefined), cwd: startupCwd, presentation: request.presentation ?? 'background', tabId: operationTabId, diff --git a/src/main/runtime/orca-runtime-create-managed-worktree.ts b/src/main/runtime/orca-runtime-create-managed-worktree.ts index 79c0b612c05..c293d65460f 100644 --- a/src/main/runtime/orca-runtime-create-managed-worktree.ts +++ b/src/main/runtime/orca-runtime-create-managed-worktree.ts @@ -68,7 +68,12 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork : null const draftStartup = !args.startup && !agentStartup && args.startupDraft - ? await this.buildStartupForDraft(repo, args.startupDraft, requestedAgent) + ? await this.buildStartupForDraft( + repo, + args.startupDraft, + requestedAgent, + args.startupLaunchSource + ) : null const effectiveStartup = args.startup ?? agentStartup?.startup ?? draftStartup?.startup const effectiveStartupFollowup = agentStartup?.followup diff --git a/src/main/runtime/orca-runtime-emit-daemon-pty-transient-fact.ts b/src/main/runtime/orca-runtime-emit-daemon-pty-transient-fact.ts index 635d14e60d3..cc112a78ade 100644 --- a/src/main/runtime/orca-runtime-emit-daemon-pty-transient-fact.ts +++ b/src/main/runtime/orca-runtime-emit-daemon-pty-transient-fact.ts @@ -51,6 +51,7 @@ export class OrcaRuntimeWithEmitDaemonPtyTransientFact extends OrcaRuntimeWithSc const pty = this.getOrCreatePtyWorktreeRecord(ptyId) if (pty) { pty.tailPendingAnsi = '' + pty.commandPaint = undefined } for (const leaf of this.getLeavesForPty(ptyId)) { leaf.tailPendingAnsi = '' diff --git a/src/main/runtime/orca-runtime-fit-override-listeners.ts b/src/main/runtime/orca-runtime-fit-override-listeners.ts index 6d4242d7758..5ef17ce4c2e 100644 --- a/src/main/runtime/orca-runtime-fit-override-listeners.ts +++ b/src/main/runtime/orca-runtime-fit-override-listeners.ts @@ -74,7 +74,6 @@ export class OrcaRuntimeWithFitOverrideListeners extends OrcaRuntimeWithStopRequ protected providerSnapshotsWithLiveModeTransition = new WeakSet() protected ptyLifecycleGenerationById = new Map() - protected pendingPtySurfaceRetirementsByPtyId = new Map() protected nextPtyLifecycleGeneration = 1 diff --git a/src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts b/src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts index a348309a568..9d6e5d56ca4 100644 --- a/src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts +++ b/src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts @@ -140,7 +140,6 @@ export class OrcaRuntimeWithInvalidateAllHandlesForPty extends OrcaRuntimeWithRe options: { awaitsRegistration?: boolean } = {} ): void { this.invalidatePtyControllerInventoryForLifecycle(ptyId) - this.pendingPtySurfaceRetirementsByPtyId.delete(ptyId) const existingPty = this.ptysById.get(ptyId) if ( existingPty && diff --git a/src/main/runtime/orca-runtime-on-pty-data.ts b/src/main/runtime/orca-runtime-on-pty-data.ts index 57b094165da..52c1fb32551 100644 --- a/src/main/runtime/orca-runtime-on-pty-data.ts +++ b/src/main/runtime/orca-runtime-on-pty-data.ts @@ -4,6 +4,7 @@ import type { TerminalOutputSourceRange } from '../../shared/terminal-output-sou import { advertisedUrlWatcher } from '../ports/advertised-url-watcher' import { appendNormalizedToTailBuffer } from './terminal-tail-buffer' import { normalizeTerminalChunk } from './terminal-ansi-normalization' +import { observeTerminalCommandPaint } from './terminal-command-paint' import { appendCompletedTerminalTranscript, buildPreview, @@ -91,6 +92,7 @@ export class OrcaRuntimeWithOnPtyData extends OrcaRuntimeWithPreparePtyExecution pty.lastOutputAt = at const normalized = normalizeTerminalChunk(data, pty.tailPendingAnsi) pty.tailPendingAnsi = normalized.pendingAnsi + observeTerminalCommandPaint(pty, data, normalized.text) const nextTail = appendNormalizedToTailBuffer( pty.tailBuffer, pty.tailPartialLine, diff --git a/src/main/runtime/orca-runtime-on-pty-exit.ts b/src/main/runtime/orca-runtime-on-pty-exit.ts index 52b7024b818..abcf2fcf57f 100644 --- a/src/main/runtime/orca-runtime-on-pty-exit.ts +++ b/src/main/runtime/orca-runtime-on-pty-exit.ts @@ -68,171 +68,172 @@ export class OrcaRuntimeWithOnPtyExit extends OrcaRuntimeWithOnClientDisconnecte pty?.incarnationId ?? `runtime:${this.runtimeId}:${this.getPtyLifecycleGeneration(ptyId)}` this.advancePtyLifecycleGeneration(ptyId) - this.notifyPtyExitListeners(ptyId) - const exactSurfaceByKey = new Map< - string, - Pick - >() - for (const [worktreeId, snapshot] of this.mobileSessionTabsByWorktree) { - for (const tab of snapshot.tabs) { - if ( - tab.type === 'terminal' && - (tab.ptyId === ptyId || tab.parentLayout?.ptyIdsByLeafId?.[tab.leafId] === ptyId) - ) { - exactSurfaceByKey.set(`${worktreeId}\0${tab.parentTabId}\0${tab.leafId}`, { - worktreeId, - parentTabId: tab.parentTabId, - leafId: tab.leafId - }) + let retirement: Promise | undefined + try { + const exactSurfaceByKey = new Map< + string, + Pick + >() + for (const [worktreeId, snapshot] of this.mobileSessionTabsByWorktree) { + for (const tab of snapshot.tabs) { + if ( + tab.type === 'terminal' && + (tab.ptyId === ptyId || tab.parentLayout?.ptyIdsByLeafId?.[tab.leafId] === ptyId) + ) { + exactSurfaceByKey.set(`${worktreeId}\0${tab.parentTabId}\0${tab.leafId}`, { + worktreeId, + parentTabId: tab.parentTabId, + leafId: tab.leafId + }) + } } } - } - for (const leaf of this.getLeavesForPty(ptyId)) { - exactSurfaceByKey.set(`${leaf.worktreeId}\0${leaf.tabId}\0${leaf.leafId}`, { - worktreeId: leaf.worktreeId, - parentTabId: leaf.tabId, - leafId: leaf.leafId - }) - } - const parsedPaneKey = parsePaneKey(pty?.paneKey ?? '') - if (pty?.tabId && parsedPaneKey) { - exactSurfaceByKey.set(`${pty.worktreeId}\0${pty.tabId}\0${parsedPaneKey.leafId}`, { - worktreeId: pty.worktreeId, - parentTabId: pty.tabId, - leafId: parsedPaneKey.leafId - }) - } - const exactSurfaces = [...exactSurfaceByKey.values()] - const pendingIncarnation = this.pendingPtyRegistrationIncarnations.get(ptyId) - const exitMatchesPendingRegistration = - this.pendingPtyRegistrationIncarnations.has(ptyId) && - (pendingIncarnation === null || - exitIncarnationId === null || - exitIncarnationId === undefined || - pendingIncarnation === exitIncarnationId) - if (exitMatchesPendingRegistration) { - // Why: reused surfaces can look registered while their replacement incarnation still awaits admission. - this.earlyExitedPtyIncarnations.set( - ptyId, - exitIncarnationId ?? pendingIncarnation ?? pty?.incarnationId ?? null - ) - } - // Why both kinds: a sleep keeps its wake hint, and a restart's replacement takes the pane. - const preservesIntentionallyStoppedSurface = - this.intentionalPtyStops.claimExit(ptyId, exitIncarnationId ?? pty?.incarnationId).length > 0 - advertisedUrlWatcher.unbindPty(ptyId) - // Clean up new mobile state for this PTY - this.mobileSubscribers.delete(ptyId) - this.terminalViewSubscribers.clearSubscribers(ptyId) - this.mobileDisplayModes.delete(ptyId) - this.resizeListeners.delete(ptyId) - this.lastRendererSizes.delete(ptyId) - this.recentPtyOutputById.delete(ptyId) - this.setupCompletionTokenByPtyId.delete(ptyId) - this.clearWaitBlockedCheckState(ptyId) - this.recentPtyPathCandidatesById.delete(ptyId) - this.ptyOutputSequenceById.delete(ptyId) - this.providerSequenceInitializedPtys.delete(ptyId) - this.providerSequenceOffsetByPtyId.delete(ptyId) - this.providerSnapshotPreferredPtys.delete(ptyId) - this.providerModeTrackersByPtyId.delete(ptyId) - this.providerModeSnapshotScansByPtyId.delete(ptyId) - this.providerBufferAcquisitionsByPtyId.delete(ptyId) - this.providerVisibleStateByPtyId.delete(ptyId) - this.providerVisibleRetryAtByPtyId.delete(ptyId) - this.agentPromptExplicitStatusFloorByPtyId.delete(ptyId) - this.ptyLifecycleGenerationById.delete(ptyId) - this.pendingPtySurfaceRetirementsByPtyId.delete(ptyId) - this.agentStatusOscProcessorsByPtyId.delete(ptyId) - this.terminalSpawnCommandsByPtyId.delete(ptyId) - this.disposePtyTitleTracker(ptyId) - this.oscTitleScanTailByPtyId.delete(ptyId) - this.osc7ScanTailByPtyId.delete(ptyId) - this.terminalCwdByPtyId.delete(ptyId) - this.terminalFileUriHostnameByPtyId.delete(ptyId) - this.wslDistroByPtyId.delete(ptyId) - // Why: a Claude agent-team leader whose PTY exits naturally (agent finished, - // process died, renderer reload) must release its team + nested panes map. - // Previously only explicit closeTerminal evicted it, so natural exits leaked - // one team per never-reused teamId for the runtime's lifetime. - const exitedTeamLeaderHandle = this.handleByPtyId.get(ptyId) - if (exitedTeamLeaderHandle) { - this.claudeAgentTeams.removeTeamForLeaderHandle(exitedTeamLeaderHandle) - } - // Layout state machine: clear `layouts` and `layoutQueues`. Any - // already-queued applyLayout work for this ptyId will run, but every - // applyLayout re-checks `layouts.has(ptyId)` (or fresh-subscribe) and - // short-circuits with `pty-exited`. - this.layouts.delete(ptyId) - this.layoutQueues.delete(ptyId) - this.freshSubscribeGuard.delete(ptyId) - this.cancelPendingDriverMutations(ptyId) - // Why: a cold restore can respawn under the same session id within the - // delayed-Enter window; the armed Enter would inject \r into the - // replacement and stamp rows it never received. - this.orchestrationMailboxNotifications.retirePty(ptyId) - // Why: the dead pty's terminal handle and any run bound to its panes still carry mailbox - // pointers; schedule a debounced repoint so they do not stay aimed at a retired session. - for (const leaf of this.getLeavesForPty(ptyId)) { - const mailboxHandle = this.handleByLeafKey.get(this.getLeafKey(leaf.tabId, leaf.leafId)) - if (mailboxHandle) { - this.mailPointerRepointScheduler.schedule(mailboxHandle) + for (const leaf of this.getLeavesForPty(ptyId)) { + exactSurfaceByKey.set(`${leaf.worktreeId}\0${leaf.tabId}\0${leaf.leafId}`, { + worktreeId: leaf.worktreeId, + parentTabId: leaf.tabId, + leafId: leaf.leafId + }) } - const boundRun = this._orchestrationDb?.getCurrentRunForPane?.(`${leaf.tabId}:${leaf.leafId}`) - if (boundRun) { - this.mailPointerRepointScheduler.schedule(`run:${boundRun.id}`) + const parsedPaneKey = parsePaneKey(pty?.paneKey ?? '') + if (pty?.tabId && parsedPaneKey) { + exactSurfaceByKey.set(`${pty.worktreeId}\0${pty.tabId}\0${parsedPaneKey.leafId}`, { + worktreeId: pty.worktreeId, + parentTabId: pty.tabId, + leafId: parsedPaneKey.leafId + }) + } + const exactSurfaces = [...exactSurfaceByKey.values()] + const pendingIncarnation = this.pendingPtyRegistrationIncarnations.get(ptyId) + const exitMatchesPendingRegistration = + this.pendingPtyRegistrationIncarnations.has(ptyId) && + (pendingIncarnation === null || + exitIncarnationId === null || + exitIncarnationId === undefined || + pendingIncarnation === exitIncarnationId) + if (exitMatchesPendingRegistration) { + // Why: reused surfaces can look registered while their replacement incarnation still awaits admission. + this.earlyExitedPtyIncarnations.set( + ptyId, + exitIncarnationId ?? pendingIncarnation ?? pty?.incarnationId ?? null + ) + } + // Why both kinds: a sleep keeps its wake hint, and a restart's replacement takes the pane. + const preservesIntentionallyStoppedSurface = + this.intentionalPtyStops.claimExit(ptyId, exitIncarnationId ?? pty?.incarnationId).length > + 0 + advertisedUrlWatcher.unbindPty(ptyId) + // Clean up new mobile state for this PTY + this.mobileSubscribers.delete(ptyId) + this.terminalViewSubscribers.clearSubscribers(ptyId) + this.mobileDisplayModes.delete(ptyId) + this.resizeListeners.delete(ptyId) + this.lastRendererSizes.delete(ptyId) + this.recentPtyOutputById.delete(ptyId) + this.setupCompletionTokenByPtyId.delete(ptyId) + this.clearWaitBlockedCheckState(ptyId) + this.recentPtyPathCandidatesById.delete(ptyId) + this.ptyOutputSequenceById.delete(ptyId) + this.providerSequenceInitializedPtys.delete(ptyId) + this.providerSequenceOffsetByPtyId.delete(ptyId) + this.providerSnapshotPreferredPtys.delete(ptyId) + this.providerModeTrackersByPtyId.delete(ptyId) + this.providerModeSnapshotScansByPtyId.delete(ptyId) + this.providerBufferAcquisitionsByPtyId.delete(ptyId) + this.providerVisibleStateByPtyId.delete(ptyId) + this.providerVisibleRetryAtByPtyId.delete(ptyId) + this.agentPromptExplicitStatusFloorByPtyId.delete(ptyId) + this.ptyLifecycleGenerationById.delete(ptyId) + this.agentStatusOscProcessorsByPtyId.delete(ptyId) + this.terminalSpawnCommandsByPtyId.delete(ptyId) + this.disposePtyTitleTracker(ptyId) + this.oscTitleScanTailByPtyId.delete(ptyId) + this.osc7ScanTailByPtyId.delete(ptyId) + this.terminalCwdByPtyId.delete(ptyId) + this.terminalFileUriHostnameByPtyId.delete(ptyId) + this.wslDistroByPtyId.delete(ptyId) + // Why: a Claude agent-team leader whose PTY exits naturally (agent finished, + // process died, renderer reload) must release its team + nested panes map. + // Previously only explicit closeTerminal evicted it, so natural exits leaked + // one team per never-reused teamId for the runtime's lifetime. + const exitedTeamLeaderHandle = this.handleByPtyId.get(ptyId) + if (exitedTeamLeaderHandle) { + this.claudeAgentTeams.removeTeamForLeaderHandle(exitedTeamLeaderHandle) + } + // Layout state machine: clear `layouts` and `layoutQueues`. Any + // already-queued applyLayout work for this ptyId will run, but every + // applyLayout re-checks `layouts.has(ptyId)` (or fresh-subscribe) and + // short-circuits with `pty-exited`. + this.layouts.delete(ptyId) + this.layoutQueues.delete(ptyId) + this.freshSubscribeGuard.delete(ptyId) + this.cancelPendingDriverMutations(ptyId) + // Why: a cold restore can respawn under the same session id within the + // delayed-Enter window; the armed Enter would inject \r into the + // replacement and stamp rows it never received. + this.orchestrationMailboxNotifications.retirePty(ptyId) + // Why: the dead pty's terminal handle and any run bound to its panes still carry mailbox + // pointers; schedule a debounced repoint so they do not stay aimed at a retired session. + for (const leaf of this.getLeavesForPty(ptyId)) { + const mailboxHandle = this.handleByLeafKey.get(this.getLeafKey(leaf.tabId, leaf.leafId)) + if (mailboxHandle) { + this.mailPointerRepointScheduler.schedule(mailboxHandle) + } + const boundRun = this._orchestrationDb?.getCurrentRunForPane?.( + `${leaf.tabId}:${leaf.leafId}` + ) + if (boundRun) { + this.mailPointerRepointScheduler.schedule(`run:${boundRun.id}`) + } } - } - if (this.terminalFitOverrides.has(ptyId)) { - this.terminalFitOverrides.delete(ptyId) - this.notifier?.terminalFitOverrideChanged(ptyId, 'desktop-fit', 0, 0) - this.notifyFitOverrideListeners(ptyId, 'desktop-fit', 0, 0) - } - // Why: clear driver state and notify the renderer so any lock banner on - // this dead pane unmounts. Without this, the pane shows a stuck banner - // until tab teardown, and `getDriver(deadPtyId)` would keep returning a - // stale `mobile{X}` to any caller that hasn't yet seen the exit IPC. - 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 - this.setPairedRendererSessionOwnership(pty.ptyId, false) - pty.disconnectedAt = Date.now() - pty.lastExitCode = exitCode - pty.lastExitCause = exitCause - // 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 - // on the dead one's idle. lastAgentStatus itself stays for `ps` display. - pty.lastAgentStatusObservedLive = false - this.resolvePtyExitWaiters(pty, ptyId) - this.pruneDisconnectedPtyTranscript(pty) - } - let retirement: Promise | undefined - if (preservesIntentionallyStoppedSurface || preservesAbnormalSshSurface) { - // Why: relay loss is recoverable; keep the HUB-owned pane addressable through the bounded reconnect grace. - this.touchMobileSessionSnapshotsForPty(ptyId, { immediate: true }) - } else { - // Why: permanent process exit is absence, not a starting/sleeping tab. - // Retire before publishing so paired clients never persist a ghost. - const pendingRetirement = {} - this.pendingPtySurfaceRetirementsByPtyId.set(ptyId, pendingRetirement) - retirement = this.retireMobileSessionSurfacesForPty(ptyId, incarnationId, exactSurfaces) - .catch((error) => { + if (this.terminalFitOverrides.has(ptyId)) { + this.terminalFitOverrides.delete(ptyId) + this.notifier?.terminalFitOverrideChanged(ptyId, 'desktop-fit', 0, 0) + this.notifyFitOverrideListeners(ptyId, 'desktop-fit', 0, 0) + } + // Why: clear driver state and notify the renderer so any lock banner on + // this dead pane unmounts. Without this, the pane shows a stuck banner + // until tab teardown, and `getDriver(deadPtyId)` would keep returning a + // stale `mobile{X}` to any caller that hasn't yet seen the exit IPC. + 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 + this.setPairedRendererSessionOwnership(pty.ptyId, false) + pty.disconnectedAt = Date.now() + pty.lastExitCode = exitCode + pty.lastExitCause = exitCause + // 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 + // on the dead one's idle. lastAgentStatus itself stays for `ps` display. + pty.lastAgentStatusObservedLive = false + this.resolvePtyExitWaiters(pty, ptyId) + this.pruneDisconnectedPtyTranscript(pty) + } + if (preservesIntentionallyStoppedSurface || preservesAbnormalSshSurface) { + // Why: relay loss is recoverable; keep the HUB-owned pane addressable through the bounded reconnect grace. + this.touchMobileSessionSnapshotsForPty(ptyId, { immediate: true }) + } else { + // Why: permanent process exit is absence, not a starting/sleeping tab. + // Retire before publishing so paired clients never persist a ghost. + try { + retirement = this.retireMobileSessionSurfacesForPty(ptyId, incarnationId, exactSurfaces) + } catch (error) { console.error('[runtime] failed to publish terminal retirement:', error) - }) - .finally(() => { - if (this.pendingPtySurfaceRetirementsByPtyId.get(ptyId) === pendingRetirement) { - this.pendingPtySurfaceRetirementsByPtyId.delete(ptyId) - } - }) + } + } + } finally { + // Why last: a stream end cues clients to re-activate the pane it ended, so the retirement + // must precede it; a cleanup fault must still end the stream. + this.notifyPtyExitListeners(ptyId) } const exitedSurfaces: { handle: string; paneKey: string | null }[] = [] diff --git a/src/main/runtime/orca-runtime-persist-terminal-surface-retirements.ts b/src/main/runtime/orca-runtime-persist-terminal-surface-retirements.ts index bafc280d957..5458543815a 100644 --- a/src/main/runtime/orca-runtime-persist-terminal-surface-retirements.ts +++ b/src/main/runtime/orca-runtime-persist-terminal-surface-retirements.ts @@ -1,95 +1,69 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. import { OrcaRuntimeWithTouchMobileSessionTabsForWorktree } from './orca-runtime-touch-mobile-session-tabs-for-worktree' import type { RetiredTerminalSurface } from './mobile-session-terminal-retirement' -import type { ExecutionHostId } from '../../shared/execution-host' import type { RuntimeMobileSessionRetiredTerminalSurface } from '../../shared/runtime-types' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { retireTerminalSurfaceFromPersistence } from './mobile-session-terminal-persistence-retirement' import { retireTerminalSurfacesFromSnapshot } from './mobile-session-terminal-retirement' import { attachRetirementProofsToSnapshot } from './mobile-session-terminal-retirement-proof' -import { cloneWorkspaceSessionState } from '../persistence/restoring-sessions/session-owner-fields' -import { rollbackWorkspaceSessionAfterFailedAsyncWrite } from '../persistence/restoring-sessions/workspace-session-write-rollback' import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' export class OrcaRuntimeWithPersistTerminalSurfaceRetirements extends OrcaRuntimeWithTouchMobileSessionTabsForWorktree { /** - * Retires each surface in the session partition of the host that owns its worktree. + * Retires each surface in the in-memory session partition of the host that owns its worktree. * Why: an SSH pane's durable surface lives in that connection's partition; retiring it * against the local partition strands the real ghost and bumps a foreign host's epoch. - * Returns null when nothing may be published because persistence is unavailable or failed. + * `accepted` held the surface; `unpersisted` had no partition to hold it (or refused the write). */ - protected async persistTerminalSurfaceRetirements( - retiredSurfaces: readonly RetiredTerminalSurface[] - ): Promise<{ accepted: RetiredTerminalSurface[]; unpersisted: RetiredTerminalSurface[] } | null> { + protected stageTerminalSurfaceRetirements(retiredSurfaces: readonly RetiredTerminalSurface[]): { + accepted: RetiredTerminalSurface[] + unpersisted: RetiredTerminalSurface[] + } { + const accepted: RetiredTerminalSurface[] = [] + const unpersisted: RetiredTerminalSurface[] = [] + for (const surface of retiredSurfaces) { + const hostId = + this.tryGetWorkspaceSessionHostIdForWorktree(surface.worktreeId) ?? LOCAL_EXECUTION_HOST_ID + const current = this.store?.getWorkspaceSession?.(hostId) + if (!current) { + unpersisted.push(surface) + continue + } + const next = retireTerminalSurfaceFromPersistence(current, surface) + if (next === current) { + continue + } + try { + this.store.setWorkspaceSession(next, hostId) + accepted.push(surface) + } catch (error) { + // Why: the process is gone whether or not the profile admits the write (quit, maintenance). + console.error('[runtime] could not stage terminal retirement:', error) + unpersisted.push(surface) + } + } + return { accepted, unpersisted } + } + + // Why no rollback: the process is gone, so a failed write leaves the retirement for the next one. + protected async persistStagedTerminalSurfaceRetirements(): Promise { if (!this.store?.runDurableMutation) { - const hasPersistedSession = retiredSurfaces.some((surface) => - this.store?.getWorkspaceSession?.( - this.tryGetWorkspaceSessionHostIdForWorktree(surface.worktreeId) ?? - LOCAL_EXECUTION_HOST_ID - ) - ) - return hasPersistedSession ? null : { accepted: [], unpersisted: [...retiredSurfaces] } + return } try { - return await this.store.runDurableMutation(() => { - const accepted: RetiredTerminalSurface[] = [] - const unpersisted: RetiredTerminalSurface[] = [] - const originals = new Map() - const staged = new Map() - for (const surface of retiredSurfaces) { - const hostId = - this.tryGetWorkspaceSessionHostIdForWorktree(surface.worktreeId) ?? - LOCAL_EXECUTION_HOST_ID - const current = this.store.getWorkspaceSession?.(hostId) - if (!current) { - unpersisted.push(surface) - continue - } - if (!this.store.setWorkspaceSession) { - throw new Error('workspace_session_unavailable') - } - if (!originals.has(hostId)) { - originals.set(hostId, cloneWorkspaceSessionState(current)) - } - const next = retireTerminalSurfaceFromPersistence(current, surface) - if (next !== current) { - this.store.setWorkspaceSession(next, hostId) - staged.set(hostId, cloneWorkspaceSessionState(this.store.getWorkspaceSession(hostId))) - accepted.push(surface) - } - } - return { - value: { accepted, unpersisted }, - persist: staged.size > 0, - rollback: () => { - for (const [hostId, stagedSession] of staged) { - const current = this.store.getWorkspaceSession(hostId) - const rolledBack = rollbackWorkspaceSessionAfterFailedAsyncWrite( - originals.get(hostId), - stagedSession, - current - ) - if (rolledBack !== current) { - this.store.setWorkspaceSession(rolledBack, hostId) - } - } - } - } - }) + // Why if-dirty: an earlier write, or another exit's, may already carry this retirement. + await this.store.runDurableMutation(() => ({ value: undefined, persist: 'if-dirty' })) } catch (error) { - console.error('[runtime] failed to persist terminal retirement:', error) - return null + console.error('[runtime] terminal retirement is not yet durable:', error) } } - protected async retireMobileSessionSurfacesForPty( + // Why synchronous: the exit's stream end cues clients to re-activate, which must find the leaf gone. + protected retireMobileSessionSurfacesForPty( ptyId: string, incarnationId: string, exactSurfaces: readonly Pick[] - ): Promise { - // Reads can mint a new frame generation while this independent retirement waits for disk. - const pendingRetirement = this.pendingPtySurfaceRetirementsByPtyId.get(ptyId) + ): Promise | undefined { const terminalHandle = this.handleByPtyId.get(ptyId) ?? this.findHandleForPtyRecord(ptyId) ?? undefined const retiredSurfaceByKey = new Map() @@ -119,26 +93,18 @@ export class OrcaRuntimeWithPersistTerminalSurfaceRetirements extends OrcaRuntim } const retiredSurfaces = [...retiredSurfaceByKey.values()] if (retiredSurfaces.length === 0) { - return + return undefined } - const persisted = await this.persistTerminalSurfaceRetirements(retiredSurfaces) - const currentIncarnation = this.ptysById.get(ptyId)?.incarnationId - if ( - !persisted || - this.pendingPtySurfaceRetirementsByPtyId.get(ptyId) !== pendingRetirement || - (currentIncarnation && currentIncarnation !== incarnationId) - ) { - return - } - for (const surface of persisted.unpersisted) { + const staged = this.stageTerminalSurfaceRetirements(retiredSurfaces) + for (const surface of staged.unpersisted) { const repoId = getRepoIdFromWorktreeId(surface.worktreeId) this.terminalTopologyRevisionByRepoId.set( repoId, (this.terminalTopologyRevisionByRepoId.get(repoId) ?? 0) + 1 ) } - // Why: one repo epoch can cover multiple exits, but only surfaces individually accepted by persistence may disappear. - const removableRetiredSurfaces = [...persisted.accepted, ...persisted.unpersisted] + // Why: one repo epoch can cover multiple exits; a surface the session binds to another PTY or incarnation stays. + const removableRetiredSurfaces = [...staged.accepted, ...staged.unpersisted] for (const [worktreeId, snapshot] of this.mobileSessionTabsByWorktree) { // Why proofs aren't gated on `removable`: the exit is the attestation, and a surface the // renderer already de-persisted leaves persistence nothing to accept. Withholding the proof @@ -163,7 +129,7 @@ export class OrcaRuntimeWithPersistTerminalSurfaceRetirements extends OrcaRuntim snapshot, ptyId, exactSurfaces: removableSurfaces, - // Why: discovery is broad by PTY id, but publication may remove only surfaces whose durable retirement was accepted. + // Why: discovery is broad by PTY id, but publication may remove only surfaces the session retired. exactOnly: true, ...(retirementProofs.length > 0 ? { retirementProofs } : {}) }) @@ -175,6 +141,7 @@ export class OrcaRuntimeWithPersistTerminalSurfaceRetirements extends OrcaRuntim } this.publishRetiredTerminalSurfaceProofs(worktreeId, retirementProofs) } + return staged.accepted.length > 0 ? this.persistStagedTerminalSurfaceRetirements() : undefined } /** Ships durable retirement proofs on their own frame when no surface removal carries them. */ diff --git a/src/main/runtime/orca-runtime-register-pty.ts b/src/main/runtime/orca-runtime-register-pty.ts index ba7b47be72d..bd9c96a5c0f 100644 --- a/src/main/runtime/orca-runtime-register-pty.ts +++ b/src/main/runtime/orca-runtime-register-pty.ts @@ -27,7 +27,6 @@ export class OrcaRuntimeWithRegisterPty extends OrcaRuntimeWithInvalidateAllHand isWsl?: boolean ): void { this.assertPtyDidNotExitBeforeRegistration(ptyId, binding?.incarnationId) - this.pendingPtySurfaceRetirementsByPtyId.delete(ptyId) this.invalidatePtyControllerInventoryForLifecycle(ptyId, connectionId) const existingPty = this.ptysById.get(ptyId) const replacementHandle = binding?.terminalHandle?.trim() 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 6acf8ba325f..167aee3e21b 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 @@ -54,12 +54,13 @@ export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRunt ) const rendererTab = this.tabs.get(tabId) const rendererLeaf = this.leaves.get(this.getLeafKey(tabId, leafId)) + // A mounted pane can publish before its PTY binds; persisted identity fences that gap. const rendererMounted = Boolean( rendererTab && rendererLeaf && runtimeWorktreeIdsEqual(rendererTab.worktreeId, worktreeId) && runtimeWorktreeIdsEqual(rendererLeaf.worktreeId, worktreeId) && - rendererLeaf.ptyId === ptyId + (rendererLeaf.ptyId === ptyId || (persisted && rendererLeaf.ptyId === null)) ) if (persisted && persistedLayout) { return { diff --git a/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts b/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts index 94dba6bdc83..eeac6dd9a78 100644 --- a/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts +++ b/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts @@ -21,6 +21,7 @@ import { resolveLocalProjectRuntimeForWorktreeId } from '../local-project-runtim import { resolveBareAgentLaunchCommand } from './runtime-agent-launch-resolution' import { buildAgentStartupPlan } from '../../shared/tui-agent-startup' import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs' +import { agentStartedTelemetry } from '../agent-launch/agent-started-telemetry' export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWithRemoveManagedWorktree { protected async resolveWorktreeRemovalTarget( @@ -250,7 +251,9 @@ export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWith ...(startupPlan.env ? { env: startupPlan.env } : {}), launchConfig: startupPlan.launchConfig, launchAgent: agent, - startupCommandDelivery: startupPlan.startupCommandDelivery + startupCommandDelivery: startupPlan.startupCommandDelivery, + // A bare command the user typed stays out of launch accounting, as before. + ...(opts.startupAgent ? { telemetry: agentStartedTelemetry(agent, opts.launchSource) } : {}) } } } diff --git a/src/main/runtime/orca-runtime-runtime-id.ts b/src/main/runtime/orca-runtime-runtime-id.ts index 4b252d57fef..d724207f921 100644 --- a/src/main/runtime/orca-runtime-runtime-id.ts +++ b/src/main/runtime/orca-runtime-runtime-id.ts @@ -48,6 +48,7 @@ import { RuntimeTerminalIdlePolls } from './runtime-terminal-idle-polls' import { TerminalIntentionalStops } from './terminal-intentional-stops' import { TerminalRunFactsRegister, type TerminalSpawnCommit } from './terminal-run-facts' import type { TuiIdleEvidenceSource } from './tui-idle-evidence' +import { hasTerminalCommandPainted } from './terminal-command-paint' import { TUI_IDLE_DEFAULT_TIMEOUT_MS, TUI_IDLE_POLL_INTERVAL_MS, @@ -351,6 +352,10 @@ export class OrcaRuntimeWithRuntimeId { ...this.tuiIdleEvidenceSource, intervalMs: TUI_IDLE_POLL_INTERVAL_MS, getForegroundProcess: (ptyId) => this.ptyController?.getForegroundProcess(ptyId) ?? null, + hasCommandPainted: (ptyId) => { + const pty = this.ptysById.get(ptyId) + return pty === undefined || hasTerminalCommandPainted(pty) + }, // Why the runtime's own emulator: a provider snapshot would be a host round trip per tick. readVisibleScreen: (ptyId) => this.headlessTerminals.has(ptyId) diff --git a/src/main/runtime/orca-runtime-terminal-retirement.test.ts b/src/main/runtime/orca-runtime-terminal-retirement.test.ts index 4422dc2a1e6..d6f70fd4c6e 100644 --- a/src/main/runtime/orca-runtime-terminal-retirement.test.ts +++ b/src/main/runtime/orca-runtime-terminal-retirement.test.ts @@ -691,7 +691,7 @@ describe('OrcaRuntimeService terminal surface retirement', () => { expect(flushOrThrow).toHaveBeenCalledOnce() }) - it('does not publish absence when the durable retirement flush fails', async () => { + it('publishes absence and reports when the durable retirement flush fails', async () => { const session = makePersistedSplitSession() const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) const runtime = new OrcaRuntimeService( @@ -717,20 +717,20 @@ describe('OrcaRuntimeService terminal surface retirement', () => { await runtime.onPtyExit('pty-left', 0, 'incarnation-a') + // Why: the process is gone either way; a failed write is reported, not reinstated. expect((await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)).tabs).toEqual([ - expect.objectContaining({ id: 'tab::left' }), expect.objectContaining({ id: 'tab::right' }) ]) - expect(events).toEqual([]) + expect(events).not.toEqual([]) expect(errorSpy).toHaveBeenCalledWith( - '[runtime] failed to persist terminal retirement:', + '[runtime] terminal retirement is not yet durable:', expect.any(Error) ) unsubscribe() errorSpy.mockRestore() }) - it('rolls back an in-memory retirement when the durable flush fails', async () => { + it('keeps the in-memory retirement when the durable flush fails', async () => { let session = makePersistedSplitSession() const original = structuredClone(session) const setWorkspaceSession = vi.fn((next: WorkspaceSessionState) => { @@ -755,10 +755,14 @@ describe('OrcaRuntimeService terminal surface retirement', () => { incarnationId: 'incarnation-a' }) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) + await runtime.onPtyExit('pty-left', 0, 'incarnation-a') - expect(session).toEqual(original) - expect(setWorkspaceSession).toHaveBeenLastCalledWith(original, LOCAL_EXECUTION_HOST_ID) - expect(setWorkspaceSession).toHaveBeenCalledTimes(2) + // Why: the next profile write carries the staged retirement; nothing may reinstate the leaf. + expect(session).not.toEqual(original) + expect(session.terminalLayoutsByTabId.tab?.ptyIdsByLeafId).toEqual({ right: 'pty-right' }) + expect(setWorkspaceSession).toHaveBeenCalledOnce() + errorSpy.mockRestore() }) }) diff --git a/src/main/runtime/orca-runtime-terminal-split-authority.test.ts b/src/main/runtime/orca-runtime-terminal-split-authority.test.ts index 9da61e59072..8369a219331 100644 --- a/src/main/runtime/orca-runtime-terminal-split-authority.test.ts +++ b/src/main/runtime/orca-runtime-terminal-split-authority.test.ts @@ -78,6 +78,7 @@ function createHarness( deferSpawn?: boolean includePairedSnapshot?: boolean rendererMounted?: boolean + rendererPtyId?: string | null graphOnlySource?: boolean sourceIncarnationId?: string stopAndWaitResult?: boolean @@ -169,7 +170,7 @@ function createHarness( worktreeId: WORKTREE_ID, leafId: SOURCE_LEAF_ID, paneRuntimeId: 1, - ptyId: SOURCE_PTY_ID + ptyId: options.rendererPtyId === undefined ? SOURCE_PTY_ID : options.rendererPtyId } ] : [], @@ -283,6 +284,19 @@ describe('remote runtime terminal split authority', () => { }) }) + it('reveals a persisted split while its mounted source is still publishing its PTY binding', async () => { + const harness = createHarness(true, { rendererMounted: true, rendererPtyId: null }) + await harness.runtime.splitTerminal(harness.handle, { direction: 'vertical' }) + expect(harness.revealTerminalSession).toHaveBeenCalledOnce() + expect(harness.getSession().terminalLayoutsByTabId[TAB_ID]?.root?.type).toBe('split') + }) + + it('does not reveal a persisted split into a renderer bound to a different PTY', async () => { + const harness = createHarness(true, { rendererMounted: true, rendererPtyId: 'replacement' }) + await harness.runtime.splitTerminal(harness.handle, { direction: 'vertical' }) + expect(harness.revealTerminalSession).not.toHaveBeenCalled() + }) + it('splits a persisted tab without consulting an unmounted host renderer', async () => { const harness = createHarness() diff --git a/src/main/runtime/orca-runtime-test-mocks/setup.spec.ts b/src/main/runtime/orca-runtime-test-mocks/setup.spec.ts index f85c353575e..c25dc33df87 100644 --- a/src/main/runtime/orca-runtime-test-mocks/setup.spec.ts +++ b/src/main/runtime/orca-runtime-test-mocks/setup.spec.ts @@ -1,6 +1,7 @@ import { expect, vi } from 'vitest' import type { Mock } from 'vitest' import type * as GitUsernameModule from '../../git/git-username' +import type * as FilesystemPathContainmentModule from '../../ipc/filesystem-path-containment' import { reviewHeadRemoteRefComponent } from '../../../shared/review-head-tracking-ref' // Why: durable review-head refs are scoped by remote identity (name + URL hash). @@ -420,10 +421,13 @@ vi.mock('../../ipc/registered-worktree-roots-cache', () => ({ invalidateAuthorizedRootsCache: invalidateAuthorizedRootsCacheMock })) -vi.mock('../../ipc/filesystem-path-containment', () => ({ - isENOENT: (error: unknown) => - Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') -})) +// Why: the real check also matches relay-rebuilt errors, which carry only the ENOENT message. +vi.mock('../../ipc/filesystem-path-containment', async () => { + const actual = await vi.importActual( + '../../ipc/filesystem-path-containment' + ) + return { isENOENT: actual.isENOENT } +}) vi.mock('../../worktree-root-preparation', () => ({ prepareLocalWorktreeRootForRepo: prepareLocalWorktreeRootForRepoMock diff --git a/src/main/runtime/orca-runtime-tests/exit-retirement-activation.spec.ts b/src/main/runtime/orca-runtime-tests/exit-retirement-activation.spec.ts new file mode 100644 index 00000000000..7da878ca2f7 --- /dev/null +++ b/src/main/runtime/orca-runtime-tests/exit-retirement-activation.spec.ts @@ -0,0 +1,148 @@ +import { describe, expect, it, vi } from 'vitest' +import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec' +import { + HEADLESS_LEAF_ID, + HEADLESS_SECOND_LEAF_ID, + TEST_WORKTREE_ID, + makeDeferred, + makeHeadlessTerminalLayout, + makeRuntimeStoreWithWorkspaceSession, + makeWorkspaceSessionWithHeadlessTerminal +} from '../orca-runtime-test-fixtures.spec' + +async function startSplitHost() { + const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( + makeWorkspaceSessionWithHeadlessTerminal({ + tabsByWorktree: { + [TEST_WORKTREE_ID]: [ + { + id: 'host-tab', + ptyId: 'pty-a', + worktreeId: TEST_WORKTREE_ID, + title: 'Split Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + terminalLayoutsByTabId: { + 'host-tab': makeHeadlessTerminalLayout({ + [HEADLESS_LEAF_ID]: 'pty-a', + [HEADLESS_SECOND_LEAF_ID]: 'pty-b' + }) + } + }) + ) + const spawn = vi.fn(async (options: { sessionId?: string }) => ({ + id: options.sessionId ?? 'fresh-pty' + })) + const adoptStablePane = vi.fn(async () => null) + const runtime = new OrcaRuntimeService(runtimeStore) + runtime.setPtyController({ + spawn, + adoptStablePane, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + listProcesses: async () => [] + }) + runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) + const activate = (leafId: string, intent: 'user' | 'automatic') => + runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', leafId, { + notifyClients: false, + navigation: 'caller', + intent + }) + const terminalLeafIds = async (): Promise => + (await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs.flatMap((tab) => + tab.type === 'terminal' ? [tab.leafId] : [] + ) + await activate(HEADLESS_LEAF_ID, 'user') + await activate(HEADLESS_SECOND_LEAF_ID, 'user') + expect(spawn).toHaveBeenCalledTimes(2) + expect(adoptStablePane).toHaveBeenCalledTimes(2) + return { runtime, runtimeStore, getSession, spawn, adoptStablePane, activate, terminalLeafIds } +} + +describe('OrcaRuntimeService', () => { + // Why: a paired mirror answers an exit's stream end by re-activating its pane. An activation + // that still finds the leaf respawns the exited session, and the retirement never publishes. + it('retires an exited split leaf before its stream end, while the durable write is pending', async () => { + const { runtime, runtimeStore, getSession, spawn, adoptStablePane, activate, terminalLeafIds } = + await startSplitHost() + + const disk = makeDeferred() + Object.assign(runtimeStore, { flushPendingOrThrowAsync: () => disk.promise }) + const published = vi.fn() + runtime.onMobileSessionTabsChanged(published) + // The stream end: the mirror's re-activation starts the moment the host releases it. + let reactivation: Promise | undefined + let observedAtStreamEnd: { binding?: string; publications: number } | undefined + runtime.subscribeToPtyExit('pty-b', () => { + observedAtStreamEnd = { + binding: + getSession().terminalLayoutsByTabId['host-tab']?.ptyIdsByLeafId?.[ + HEADLESS_SECOND_LEAF_ID + ], + publications: published.mock.calls.length + } + reactivation = activate(HEADLESS_SECOND_LEAF_ID, 'automatic').catch((error) => error) + }) + const exiting = runtime.onPtyExit('pty-b', 0, undefined, { providerExitObserved: true }) + + // Why: whatever answers the stream end, over any transport, must already see the leaf retired. + expect(observedAtStreamEnd).toEqual({ binding: undefined, publications: 1 }) + expect(reactivation).toBeDefined() + // Why: the refusal must come from the lookup, before any stable-pane adoption can revive it. + expect(await reactivation).toEqual(new Error('tab_not_found')) + expect(adoptStablePane).toHaveBeenCalledTimes(2) + expect(spawn).toHaveBeenCalledTimes(2) + expect(await terminalLeafIds()).toEqual([HEADLESS_LEAF_ID]) + expect( + getSession().terminalLayoutsByTabId['host-tab']?.ptyIdsByLeafId?.[HEADLESS_SECOND_LEAF_ID] + ).toBeUndefined() + + disk.resolve() + await exiting + expect(await terminalLeafIds()).toEqual([HEADLESS_LEAF_ID]) + }) + + // Why: the process is gone whether or not the profile admits the write (quit, maintenance). + it('retires the pane and ends the stream when the profile refuses the staging write', async () => { + const { runtime, runtimeStore, activate, terminalLeafIds } = await startSplitHost() + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) + runtimeStore.setWorkspaceSession.mockImplementation(() => { + throw new Error('Profile maintenance or finalization is blocking new terminal snapshot work') + }) + const streamEnd = vi.fn() + runtime.subscribeToPtyExit('pty-b', streamEnd) + + await runtime.onPtyExit('pty-b', 0, undefined, { providerExitObserved: true }) + + expect(streamEnd).toHaveBeenCalledOnce() + expect(await terminalLeafIds()).toEqual([HEADLESS_LEAF_ID]) + await expect(activate(HEADLESS_SECOND_LEAF_ID, 'automatic')).rejects.toThrow('tab_not_found') + expect(errorSpy).toHaveBeenCalledWith( + '[runtime] could not stage terminal retirement:', + expect.any(Error) + ) + errorSpy.mockRestore() + }) + + // Why: the stream end now waits behind the exit cleanup; a cleanup fault must not strand it. + it('still ends the stream when exit cleanup throws before the retirement', () => { + const runtime = new OrcaRuntimeService() + Object.assign(runtime, { + disposeHeadlessTerminal: () => { + throw new Error('dispose_failed') + } + }) + const streamEnd = vi.fn() + runtime.subscribeToPtyExit('pty-a', streamEnd) + + expect(() => runtime.onPtyExit('pty-a', 0)).toThrow('dispose_failed') + expect(streamEnd).toHaveBeenCalledOnce() + }) +}) diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-12.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-12.spec.ts index 6aa0c6bd70f..92612c2e763 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-12.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-12.spec.ts @@ -223,4 +223,74 @@ describe('OrcaRuntimeService', () => { expect(spawn.mock.calls[0]?.[0]?.command).not.toMatch(/--model/) }) + + // Every agent the runtime builds is attributed, so a launch whose caller named no surface still + // counts; only the caller's surface is taken, the rest is derived from the agent it resolved. + it.each([ + ['orchestration', 'orchestration'], + [undefined, 'unknown'], + ['a_surface_added_later', 'unknown'] + ])('attributes a startup-agent launch named %s as %s', async (launchSource, expected) => { + const spawn = vi.fn().mockResolvedValue({ id: 'pty-attributed' }) + const runtime = new OrcaRuntimeService(store) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + startupAgent: 'claude', + ...(launchSource ? { launchSource } : {}) + }) + + expect(spawn.mock.calls[0]?.[0]?.telemetry).toEqual({ + agent_kind: 'claude-code', + launch_source: expected, + request_kind: 'new' + }) + }) + + it('attributes a fresh agent session the host builds as unknown', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'pty-session' }) + const runtime = new OrcaRuntimeService(store) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + await runtime.createAgentSession( + { + clientOperationId: `${Date.now()}-${'cd'.repeat(16)}`, + worktree: `id:${TEST_WORKTREE_ID}`, + agent: 'claude' + }, + { clientId: 'remote-desktop', clientKind: 'runtime' } + ) + + expect(spawn.mock.calls[0]?.[0]?.telemetry).toEqual({ + agent_kind: 'claude-code', + launch_source: 'unknown', + request_kind: 'new' + }) + }) + + it('leaves a bare agent command the user typed out of launch attribution', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'pty-bare' }) + const runtime = new OrcaRuntimeService(store) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude' }) + + expect(spawn.mock.calls[0]?.[0]?.launchAgent).toBe('claude') + expect(spawn.mock.calls[0]?.[0]?.telemetry).toBeUndefined() + }) }) diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts index cc2c2cae2ba..d3d2ee8b44a 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts @@ -509,6 +509,7 @@ describe('OrcaRuntimeService', () => { repoSelector: 'id:repo-1', name: 'runtime-explicit-draft', startupDraft: draftUrl, + startupLaunchSource: 'cli', createdWithAgent: 'codex', activate: true }) @@ -519,7 +520,9 @@ describe('OrcaRuntimeService', () => { expect.objectContaining({ cwd: '/tmp/workspaces/runtime-explicit-draft', command: "codex '--dangerously-bypass-approvals-and-sandbox'", - worktreeId: result.worktree.id + worktreeId: result.worktree.id, + // The host picked and launched this agent, so it carries the caller's surface too. + telemetry: { agent_kind: 'codex', launch_source: 'cli', request_kind: 'new' } }) ) expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'codex' }) diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-05.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-05.spec.ts index c4ac5ea9a84..d341496786e 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-05.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-05.spec.ts @@ -189,7 +189,11 @@ describe('OrcaRuntimeService', () => { } const fsProvider = { realpath: vi.fn().mockResolvedValue('/remote/mobile-codex-draft'), - readFile: vi.fn().mockRejectedValue(new Error('missing config')), + readFile: vi + .fn() + .mockRejectedValue( + new Error("ENOENT: no such file or directory, stat '/home/dev/.codex/config.toml'") + ), createDir: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined) } @@ -302,7 +306,11 @@ describe('OrcaRuntimeService', () => { } const fsProvider = { realpath: vi.fn().mockResolvedValue('/remote/mobile-codex-command'), - readFile: vi.fn().mockRejectedValue(new Error('missing config')), + readFile: vi + .fn() + .mockRejectedValue( + new Error("ENOENT: no such file or directory, stat '/home/dev/.codex/config.toml'") + ), createDir: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined) } diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 6d4cba4741d..070363fdb3f 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -79,6 +79,7 @@ await import('./orca-runtime-tests/mobile-session-tabs-part-10.spec') await import('./orca-runtime-tests/mobile-session-tabs-part-11.spec') await import('./orca-runtime-tests/mobile-session-tabs-part-12.spec') await import('./orca-runtime-tests/mobile-session-tabs-part-13.spec') +await import('./orca-runtime-tests/exit-retirement-activation.spec') await import('./orca-runtime-tests/mobile-creation-and-orchestration.spec') await import('./orca-runtime-tests/mobile-creation-and-orchestration-part-02.spec') await import('./orca-runtime-tests/mobile-creation-and-orchestration-part-03.spec') diff --git a/src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts b/src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts index 6764530d038..7885614384b 100644 --- a/src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts +++ b/src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts @@ -48,7 +48,7 @@ describe('structured mailbox pointer host', () => { // The defect this pins: a running turn is announced by ONE lifecycle item, and settlement // tombstones it rather than rewriting it. A long tool-calling turn pushes that item arbitrarily // far from the tail, so any page-sized read reports a busy worker as idle — and the pointer is - // then delivered mid-turn, which Codex coalesces into the running turn and Claude queues behind + // then delivered mid-turn, which Codex coalesces into the running turn and Claude folds into // it -- either way folded into work already in flight rather than read as a new instruction. const items = [runningTurn(), ...transcript(500)] hostRef.current = { journalSnapshot: () => ({ items, submissions: [] }) } diff --git a/src/main/runtime/orchestration/structured-session-pointer-delivery.ts b/src/main/runtime/orchestration/structured-session-pointer-delivery.ts index e66c288ff58..903ae84f9eb 100644 --- a/src/main/runtime/orchestration/structured-session-pointer-delivery.ts +++ b/src/main/runtime/orchestration/structured-session-pointer-delivery.ts @@ -128,7 +128,8 @@ export function structuredSessionGateFacts( * them. Neither refuses the frame: Codex COALESCES a mid-turn `turn/start` into * the running turn -- measured on codex-cli 0.147.0, 0.150.1 and 0.153.4, none * of which refuse it and none of which fire a second `turn/started` -- and - * Claude queues it behind the turn. Both therefore + * Claude folds it into the running turn (or runs it as the next turn when the + * turn ends first). Both therefore * fold the nudge into work already in flight, where it reads as part of the * running turn rather than a new instruction. Waiting for the turn to settle is * the one contract that holds for both, and it preserves orchestration's diff --git a/src/main/runtime/orchestration/structured-worker-group-addressing.test.ts b/src/main/runtime/orchestration/structured-worker-group-addressing.test.ts index 1c5dc4d6435..43b7fe2be45 100644 --- a/src/main/runtime/orchestration/structured-worker-group-addressing.test.ts +++ b/src/main/runtime/orchestration/structured-worker-group-addressing.test.ts @@ -166,7 +166,7 @@ describe('group addressing and structured workers', () => { // The same trap that already cost this branch once: settlement tombstones the lifecycle item // rather than rewriting it, so a long tool-calling turn pushes it arbitrarily far from the // tail and any page-sized read reports a BUSY worker as idle — then `@idle` broadcasts into a - // running turn, which Codex refuses outright and Claude queues behind. + // running turn, which Codex refuses outright and Claude folds into it. registerWorker() installHost({ items: [runningTurn(), ...transcript(500)] }) expect(await structuredWorkerAgentStatus(SESSION_ID)).toBe('working') diff --git a/src/main/runtime/orchestration/structured-worker-group-addressing.ts b/src/main/runtime/orchestration/structured-worker-group-addressing.ts index 4115f168552..3a6161f8187 100644 --- a/src/main/runtime/orchestration/structured-worker-group-addressing.ts +++ b/src/main/runtime/orchestration/structured-worker-group-addressing.ts @@ -68,8 +68,8 @@ export function listAddressableStructuredWorkers( * A structured worker's agent status, in the vocabulary `@idle` already matches on. * * Null when the session cannot be read: unknown must not read as idle, or a broadcast to `@idle` - * would wake a worker mid-turn — which Codex coalesces into the running turn and Claude queues - * behind it. + * would wake a worker mid-turn — which Codex coalesces into the running turn and Claude folds + * into it. */ export async function structuredWorkerAgentStatus(sessionId: string): Promise { const facts = await readStructuredSessionGateFacts(sessionId) diff --git a/src/main/runtime/orchestration/worker-transcript-payload.test.ts b/src/main/runtime/orchestration/worker-transcript-payload.test.ts index 802b724a2aa..d0b4ce23127 100644 --- a/src/main/runtime/orchestration/worker-transcript-payload.test.ts +++ b/src/main/runtime/orchestration/worker-transcript-payload.test.ts @@ -299,3 +299,48 @@ describe('worker transcript wire bounds', () => { ) }) }) + +describe("worker transcript wire bounds — a subagent's line names its agent and nothing more", () => { + it('serves the producing agent id, bounded like the roster key, and drops provenance', () => { + const longId = `task-${'x'.repeat(2_000)}` + const result = boundWorkerTranscriptMessages([ + { + id: 'child-line', + role: 'assistant', + timestamp: null, + source: 'transcript', + blocks: [{ type: 'text', text: 'The PR is CLEAN.' }], + agentId: longId, + parentAgentId: 'task-parent', + providerParentRef: 'toolu_provider_call', + producerKind: 'agent', + attempt: 2 + }, + { + id: 'roster', + role: 'system', + timestamp: null, + source: 'transcript', + blocks: [ + { + type: 'subagent-group', + groupId: 'group-1', + agents: [{ id: longId, label: 'review the PR', state: 'working' }] + } + ] + } + ]) + const [child, roster] = result.messages + expect(child).not.toHaveProperty('providerParentRef') + expect(child).not.toHaveProperty('parentAgentId') + expect(child).not.toHaveProperty('producerKind') + expect(child).not.toHaveProperty('attempt') + // The line's agent id and the roster entry that names it bound to the same key, + // so the reader can still put a name to the line. + const rosterBlock = roster?.blocks[0] + expect(rosterBlock?.type).toBe('subagent-group') + const entryId = rosterBlock?.type === 'subagent-group' ? rosterBlock.agents[0]?.id : undefined + expect(child?.agentId).toBeDefined() + expect(child?.agentId).toBe(entryId) + }) +}) diff --git a/src/main/runtime/orchestration/worker-transcript-payload.ts b/src/main/runtime/orchestration/worker-transcript-payload.ts index 684dfc771e2..b62687bd813 100644 --- a/src/main/runtime/orchestration/worker-transcript-payload.ts +++ b/src/main/runtime/orchestration/worker-transcript-payload.ts @@ -1,5 +1,6 @@ import { createHash } from 'node:crypto' import type { NativeChatBlock, NativeChatMessage } from '../../../shared/native-chat-types' +import { agentJournalItemSubagentId } from '../../../shared/agent-session-journal-producer' import { boundSubagentEntryId } from '../../native-chat/subagent-entry-id-bounds' import { boundWorkerTranscriptActivityBlock } from './worker-transcript-activity-block-bounds' @@ -109,11 +110,24 @@ function boundMessage( markClipped(state, 'Some transcript blocks were omitted from oversized messages.') } // The journal position only orders a live list; a worker read is already in order. - const { journalPosition: _journalPosition, ...served } = message + // Of the producer linkage only the agent's id is served, so a reader can say whose + // line it is; the rest is provenance. + const { + journalPosition: _journalPosition, + agentId: _agentId, + parentAgentId: _parentAgentId, + providerParentRef: _providerParentRef, + producerKind: _producerKind, + attempt: _attempt, + ...served + } = message + const subagentId = agentJournalItemSubagentId(message) return { ...served, id: boundIdentifier(message.id, transcriptPath, state), ...(message.turnId ? { turnId: boundIdentifier(message.turnId, transcriptPath, state) } : {}), + // Same key as the roster entry that names it, so it is bounded the same way. + ...(subagentId === null ? {} : { agentId: boundEntryId(subagentId, state) }), blocks: blocks.map((block) => boundBlock(block, state)) } } diff --git a/src/main/runtime/rpc/methods/agent-launch-surfaces.ts b/src/main/runtime/rpc/methods/agent-launch-surfaces.ts index 07dcdb53186..3f184cd50b5 100644 --- a/src/main/runtime/rpc/methods/agent-launch-surfaces.ts +++ b/src/main/runtime/rpc/methods/agent-launch-surfaces.ts @@ -14,10 +14,6 @@ */ import { randomUUID } from 'node:crypto' -import { tuiAgentToAgentKind } from '../../../../shared/agent-kind' -import { launchSourceSchema } from '../../../../shared/telemetry-property-schemas' -import type { TuiAgent } from '../../../../shared/tui-agent' -import type { TerminalCreateOptions } from '../../runtime-terminal-contracts' import { narrowStructuredLaunchSeedOptions } from '../../../../shared/native-chat-session-option-defaults' import { createStructuredAgentSessionOperationId } from '../../../../shared/structured-agent-session-mutation' import { structuredAgentSessionTabId } from '../../../../shared/structured-agent-session-projection' @@ -138,7 +134,7 @@ export function agentLaunchSurfaceFactory( ...(launchPreferences ? { launchPreferences } : {}), // A live reserved pane would be attached, not launched into, so the runtime refuses it. ...(paneKey ? { ...paneIdentity(paneKey), requireFreshPane: true } : {}), - ...agentLaunchTelemetry(agent, launchSource), + ...(launchSource ? { launchSource } : {}), onPtySpawnDispatched: terminalSpawn.onPtySpawnDispatched }) const terminal = await created.catch(terminalSpawn.rethrow) @@ -159,34 +155,6 @@ export function agentLaunchSurfaceFactory( } } -/** - * The `agent_started` triple, of which only `launch_source` came from the caller. - * - * `agent_kind` and `request_kind` are derived rather than accepted — the host already knows both, - * and a value it derives is a value a caller cannot misreport. `request_kind` is always `new` - * because a launch reusing a terminal returns before any surface is created. - * - * An unrecognized `launch_source` drops the telemetry and starts the agent anyway. The wire keeps - * the arm set open so an older host cannot refuse a newer client's launch over a label, which is - * only honoured if the refusal does not reappear here: attribution is bookkeeping, and bookkeeping - * must not gate the user's launch. - */ -function agentLaunchTelemetry( - agent: TuiAgent, - launchSource: string | undefined -): Pick { - const parsed = launchSourceSchema.safeParse(launchSource) - return parsed.success - ? { - telemetry: { - agent_kind: tuiAgentToAgentKind(agent), - launch_source: parsed.data, - request_kind: 'new' - } - } - : {} -} - function requireInstalledHost(): StructuredAgentSessionHost { const host = getStructuredAgentSessionHost() if (!host) { diff --git a/src/main/runtime/rpc/methods/agent-launch.test.ts b/src/main/runtime/rpc/methods/agent-launch.test.ts index b39dbc350f0..75045cf74c4 100644 --- a/src/main/runtime/rpc/methods/agent-launch.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch.test.ts @@ -331,6 +331,23 @@ describe('the worktree factory', () => { }) }) + it("attributes a create by the launch's own source, never a copy inside the create payload", async () => { + const createWithSource = { + ...CREATE_LAUNCH, + target: { + kind: 'create-worktree', + create: { ...CREATE_LAUNCH.target.create, launchSource: 'cli' } + } + } + const named = runtimeStub({ settings: {} }) + await launch({ ...createWithSource, launchSource: 'onboarding' }, named) + expect(createArgs(named)).toMatchObject({ startupLaunchSource: 'onboarding' }) + + const unnamed = runtimeStub({ settings: {} }) + await launch(createWithSource, unnamed) + expect(createArgs(unnamed)).not.toHaveProperty('startupLaunchSource') + }) + it('preserves an explicit no-arguments value for an agent-first worktree create', async () => { const runtime = runtimeStub({ settings: {} }) await launch({ ...CREATE_LAUNCH, agentArgs: null }, runtime) @@ -554,15 +571,14 @@ describe('launch inputs that cross the wire', () => { }) }) - it('derives agent_kind and request_kind, taking only launch_source from the caller', async () => { + it("hands the caller's launch_source to the runtime, which attributes the launch", async () => { const runtime = runtimeStub({ settings: {} }) await launch({ ...EXISTING_LAUNCH, launchSource: 'source_control_recovery' }, runtime) - expect(terminalOptions(runtime).telemetry).toEqual({ - agent_kind: 'claude-code', - launch_source: 'source_control_recovery', - request_kind: 'new' - }) + // The runtime derives agent_kind and request_kind from the agent it builds, so the handler + // forwards only the one member it cannot know, and never a prebuilt triple. + expect(terminalOptions(runtime)).toMatchObject({ launchSource: 'source_control_recovery' }) + expect(terminalOptions(runtime)).not.toHaveProperty('telemetry') }) it('starts the agent anyway when launch_source is one this build has never heard of', async () => { @@ -573,16 +589,16 @@ describe('launch inputs that cross the wire', () => { ) // The whole point of the open arm set: attribution is bookkeeping, and bookkeeping must never - // gate a user action. The row is dropped; the launch is not. + // gate a user action. The runtime records it as `unknown`; the launch still starts. expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_1' }) - expect(terminalOptions(runtime)).not.toHaveProperty('telemetry') + expect(terminalOptions(runtime)).toMatchObject({ launchSource: 'a_surface_added_later' }) }) - it('sends no telemetry at all when the caller named no launch source', async () => { + it('names no launch source when the caller named none', async () => { const runtime = runtimeStub({ settings: {} }) await launch(EXISTING_LAUNCH, runtime) - expect(terminalOptions(runtime)).not.toHaveProperty('telemetry') + expect(terminalOptions(runtime)).not.toHaveProperty('launchSource') }) it('keeps a structured preference when the cwd names the workspace root', async () => { diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation.ts index 6f7a1bba077..067f583e37b 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation.ts @@ -114,6 +114,7 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS = [ observeSetupCompletion: true, createdWithAgent: agent as TuiAgent, startupAgent: agent as TuiAgent, + startupLaunchSource: 'orchestration', ...(launch.preferences ? { startupLaunchPreferences: launch.preferences } : {}), activate: false, lineage: { noParent: true } @@ -185,6 +186,7 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS = [ // Why: agent ids are not shell commands (`cursor` is the desktop app, // its CLI is `cursor-agent`); resolve through the TUI agent config. startupAgent: agent as TuiAgent, + launchSource: 'orchestration', ...(launch.preferences ? { launchPreferences: launch.preferences } : {}), title: `worker-${params.taskId}`, presentation: 'background' diff --git a/src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts index 8459a9886e4..d265858b024 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts @@ -155,6 +155,7 @@ describe('orchestration RPC methods', () => { // the tab without scrolling the sidebar to the worker's workspace. expect(runtime.createTerminal).toHaveBeenCalledWith('id:repo::worktree', { startupAgent: 'codex', + launchSource: 'orchestration', title: `worker-${task.id}`, surfaceOwner: false }) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts index b18eb9ee6c2..3e86dea3e28 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts @@ -69,6 +69,7 @@ export async function createExistingWorktreeWorkerTerminal(args: { // desktop app while its CLI is `cursor-agent`. Let the runtime build the // configured launcher instead of executing the raw id. startupAgent: args.agent, + launchSource: 'orchestration', ...(args.launchPreferences ? { launchPreferences: args.launchPreferences } : {}), title: `worker-${args.taskId}`, // Why: dispatching a worker is background work; it must not pull the sidebar diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts index fe0c11e374d..cbdab8d1f94 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts @@ -61,6 +61,7 @@ export async function createWorkerWorktree(args: { ...(args.withAgentTerminal ? { startupAgent: args.agent, + startupLaunchSource: 'orchestration', ...(args.launchPreferences ? { startupLaunchPreferences: args.launchPreferences } : {}) } : {}), diff --git a/src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts index 5164ac0b22f..cda3354e816 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts @@ -140,6 +140,7 @@ describe('orchestration new-worktree workers', () => { expect(runtime.createManagedWorktree).toHaveBeenCalledWith( expect.objectContaining({ startupAgent: 'codex', + startupLaunchSource: 'orchestration', awaitTerminalProvisioning: true, observeSetupCompletion: true, lineage: expect.objectContaining({ noParent: true, parentWorktree: undefined }) diff --git a/src/main/runtime/rpc/methods/session-tabs-unsubscribe.test.ts b/src/main/runtime/rpc/methods/session-tabs-unsubscribe.test.ts index 8490181d0ea..021224bd649 100644 --- a/src/main/runtime/rpc/methods/session-tabs-unsubscribe.test.ts +++ b/src/main/runtime/rpc/methods/session-tabs-unsubscribe.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { OrcaRuntimeService } from '../../orca-runtime' +import { RuntimeSubscriptionRegistry } from '../../runtime-subscription-registry' import { RpcDispatcher } from '../dispatcher' import { SESSION_TAB_METHODS } from './session-tabs' @@ -61,11 +62,61 @@ describe('session tab unsubscribe RPC methods', () => { expect(cleanupSubscription).toHaveBeenCalledWith('session.tabs:conn-1:*:sub-all-1') expect(cleanupSubscriptionsByPrefix).not.toHaveBeenCalled() }) + + it('ends only the named stream when a newer one watches the same worktree', async () => { + const { dispatcher, ends } = await subscribeTwiceToOneWorktree() + + await dispatcher.dispatchStreaming( + request('session.tabs.unsubscribe', { worktree: 'id:wt-1', subscriptionId: 'sub-old' }), + vi.fn(), + { connectionId: 'conn-1' } + ) + + await vi.waitFor(() => expect(ends['sub-old']).toBe(1)) + expect(ends['sub-new']).toBe(0) + }) + + it('ends every stream for the worktree when the unsubscribe names no request', async () => { + const { dispatcher, ends } = await subscribeTwiceToOneWorktree() + + await dispatcher.dispatchStreaming( + request('session.tabs.unsubscribe', { worktree: 'id:wt-1' }), + vi.fn(), + { connectionId: 'conn-1' } + ) + + await vi.waitFor(() => expect(ends).toEqual({ 'sub-old': 1, 'sub-new': 1 })) + }) }) +async function subscribeTwiceToOneWorktree() { + const registry = new RuntimeSubscriptionRegistry() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Partial runtime backed by the real subscription registry; it supplies every member session.tabs subscribe/unsubscribe call. + const runtime = { + ...runtimeWithCleanup(registry.cleanup.bind(registry), registry.cleanupByPrefix.bind(registry)), + registerSubscriptionCleanup: registry.register.bind(registry), + getSubscriptionRegistrationVersion: registry.getRegistrationVersion.bind(registry), + onMobileSessionTabsChanged: () => () => {} + } as unknown as OrcaRuntimeService + const dispatcher = new RpcDispatcher({ runtime, methods: SESSION_TAB_METHODS }) + const ends: Record = { 'sub-old': 0, 'sub-new': 0 } + for (const id of ['sub-old', 'sub-new']) { + await dispatcher.dispatchStreaming( + { ...request('session.tabs.subscribe', { worktree: 'id:wt-1' }), id }, + (message) => { + if (JSON.parse(message).result?.type === 'end') { + ends[id]! += 1 + } + }, + { connectionId: 'conn-1' } + ) + } + return { dispatcher, ends } +} + function runtimeWithCleanup( - cleanupSubscription: ReturnType, - cleanupSubscriptionsByPrefix = vi.fn() + cleanupSubscription: (id: string) => void, + cleanupSubscriptionsByPrefix: (prefix: string, throughVersion?: number) => void = vi.fn() ): OrcaRuntimeService { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the unsubscribe methods reach only these members; a missing one throws and fails the test. return { diff --git a/src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts index df00957215f..1cc5050d803 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts @@ -424,7 +424,10 @@ describe('an agent exit', () => { { kind: 'turn', turnId: 'working', state: 'running' } ) await rig.host.flushStreamedEvents(SESSION) - vi.spyOn(open.journal, 'appendLifecycleBatch').mockRejectedValueOnce(new Error('disk full')) + // The exit's own write and the retry recording the exit queues are both refused. + vi.spyOn(open.journal, 'appendLifecycleBatch') + .mockRejectedValueOnce(new Error('disk full')) + .mockRejectedValueOnce(new Error('disk full')) await rig.host.handleAdapterEvent({ type: 'ended', sessionId: SESSION, @@ -440,6 +443,7 @@ describe('an agent exit', () => { deathEvidence: { kind: 'exit-observed' } }) ) + await rig.host.collaboratorsForTests().serialize(SESSION, async () => {}) const sent = await rig.host.send( CALLER, diff --git a/src/main/runtime/rpc/methods/worktree-create-args.ts b/src/main/runtime/rpc/methods/worktree-create-args.ts index 932659758bb..c05dc906f40 100644 --- a/src/main/runtime/rpc/methods/worktree-create-args.ts +++ b/src/main/runtime/rpc/methods/worktree-create-args.ts @@ -78,6 +78,7 @@ export function buildManagedWorktreeCreateArgs( : undefined, ...(params.startupAgent ? { startupAgent: params.startupAgent } : {}), ...(params.startupPrompt !== undefined ? { startupPrompt: params.startupPrompt } : {}), + ...(params.launchSource ? { startupLaunchSource: params.launchSource } : {}), startupDraft: params.startupDraft, lineage: { parentWorkspace: params.parentWorkspace, diff --git a/src/main/runtime/rpc/methods/worktree-create-launch-source.test.ts b/src/main/runtime/rpc/methods/worktree-create-launch-source.test.ts new file mode 100644 index 00000000000..2cfd6c3de92 --- /dev/null +++ b/src/main/runtime/rpc/methods/worktree-create-launch-source.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from 'vitest' +import { RpcDispatcher } from '../dispatcher' +import type { OrcaRuntimeService } from '../../orca-runtime' +import { WORKTREE_METHODS } from './worktree' + +const repo = { + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 1, + kind: 'git' as const +} + +describe('worktree.create launch source', () => { + it('hands the surface that asked for a startup agent to the runtime', async () => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture implements every runtime method worktree.create reaches for a create with no provenance request. + const runtime = { + getRuntimeId: () => 'test-runtime', + dedupeWorktreeCreate: (_repo: string, _id: string | undefined, run: () => Promise) => + run(), + showRepo: vi.fn().mockResolvedValue(repo), + createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } }) + } as unknown as OrcaRuntimeService + const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS }) + + // An open label: a surface this host has never heard of must not refuse the create. + for (const launchSource of ['cli', 'a_surface_added_later']) { + const response = await dispatcher.dispatch({ + id: 'req-1', + authToken: 'tok', + method: 'worktree.create', + params: { repo: 'repo-1', name: 'agent-startup', startupAgent: 'codex', launchSource } + }) + expect(response).toMatchObject({ ok: true }) + expect(runtime.createManagedWorktree).toHaveBeenLastCalledWith( + expect.objectContaining({ startupAgent: 'codex', startupLaunchSource: launchSource }) + ) + } + }) +}) diff --git a/src/main/runtime/runtime-managed-worktree-create-types.ts b/src/main/runtime/runtime-managed-worktree-create-types.ts index bdc8c2a0a12..9edd5b23836 100644 --- a/src/main/runtime/runtime-managed-worktree-create-types.ts +++ b/src/main/runtime/runtime-managed-worktree-create-types.ts @@ -54,6 +54,7 @@ export type RuntimeManagedWorktreeCreateArgs = { /** Per-launch inputs used when `startupAgent` is the created terminal surface. */ startupAgentArgs?: string | null startupCwd?: string + /** The surface behind a host-built startup agent (`startupAgent` or `startupDraft`). */ startupLaunchSource?: string /** A caller-minted `tabId:leafId` for the startup terminal's pane. */ startupPaneKey?: string diff --git a/src/main/runtime/runtime-terminal-contracts.ts b/src/main/runtime/runtime-terminal-contracts.ts index fa0e7a0afa0..e08fffa273c 100644 --- a/src/main/runtime/runtime-terminal-contracts.ts +++ b/src/main/runtime/runtime-terminal-contracts.ts @@ -62,6 +62,9 @@ export type TerminalCreateOptions = { viewMode?: 'terminal' | 'chat' startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] telemetry?: WorktreeStartupLaunch['telemetry'] + /** The surface that asked for this `startupAgent` launch; the runtime attributes every one it + * builds, as `unknown` when this is absent or unrecognized. Ignored without `startupAgent`. */ + launchSource?: string title?: string focus?: boolean rendererBacked?: boolean diff --git a/src/main/runtime/runtime-terminal-idle-polls.test.ts b/src/main/runtime/runtime-terminal-idle-polls.test.ts index cab8e75ae46..5149c63da22 100644 --- a/src/main/runtime/runtime-terminal-idle-polls.test.ts +++ b/src/main/runtime/runtime-terminal-idle-polls.test.ts @@ -4,6 +4,7 @@ import type { TerminalWaiter } from './runtime-terminal-contracts' import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' import type { RuntimeTerminalWait } from '../../shared/runtime-types' import type { TuiAgent } from '../../shared/tui-agent' +import { TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' const INTERVAL_MS = 2000 @@ -71,6 +72,7 @@ describe('RuntimeTerminalIdlePolls timer budget', () => { quiescenceMs: 1500, getTabTitle: () => null, getForegroundProcess: () => null, + hasCommandPainted: () => true, getAdoptedPtyIdleStatus: () => null, getPaneAgent: () => null, getFirstPartyAgentStatus: () => null, @@ -108,6 +110,7 @@ describe('RuntimeTerminalIdlePolls timer budget', () => { quiescenceMs: 1500, getTabTitle: () => null, getForegroundProcess: () => null, + hasCommandPainted: () => true, getAdoptedPtyIdleStatus: () => null, getPaneAgent: () => null, getFirstPartyAgentStatus: () => null, @@ -135,6 +138,7 @@ describe('RuntimeTerminalIdlePolls timer budget', () => { quiescenceMs: 1500, getTabTitle: () => null, getForegroundProcess: () => null, + hasCommandPainted: () => true, getAdoptedPtyIdleStatus: () => null, getPaneAgent: () => null, getFirstPartyAgentStatus: () => null, @@ -167,6 +171,7 @@ describe('RuntimeTerminalIdlePolls timer budget', () => { new Promise((resolve) => { gates.push(resolve) }), + hasCommandPainted: () => true, getAdoptedPtyIdleStatus: () => null, getPaneAgent: () => null, getFirstPartyAgentStatus: () => null, @@ -225,6 +230,7 @@ describe('RuntimeTerminalIdlePolls rendered-screen blocked prompts', () => { getTabTitle: () => null, // Unknown agent + quiet pane: without the screen check this would settle idle. getForegroundProcess: () => (foreground ? Promise.resolve(foreground) : null), + hasCommandPainted: () => true, getAdoptedPtyIdleStatus: () => null, getPaneAgent: () => agent, getFirstPartyAgentStatus: () => null, @@ -354,3 +360,77 @@ describe('RuntimeTerminalIdlePolls rendered-screen blocked prompts', () => { expect(resolved).toEqual([expect.objectContaining({ satisfied: true })]) }) }) + +describe('RuntimeTerminalIdlePolls quiet foreground for a launched agent', () => { + const QUIESCENCE_MS = 1500 + + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + function createPolls( + agent: TuiAgent, + resolved: string[], + foregroundReads: string[] = [] + ): RuntimeTerminalIdlePolls { + return new RuntimeTerminalIdlePolls({ + intervalMs: INTERVAL_MS, + quiescenceMs: QUIESCENCE_MS, + getTabTitle: () => null, + getForegroundProcess: (ptyId) => { + foregroundReads.push(ptyId) + return Promise.resolve(TUI_AGENT_CONFIG[agent].expectedProcess) + }, + hasCommandPainted: () => true, + getAdoptedPtyIdleStatus: () => null, + getPaneAgent: () => agent, + getFirstPartyAgentStatus: () => null, + readScreenLines: () => null, + readVisibleScreen: () => null, + getLiveLeaf: (leaf) => leaf, + resolve: (waiter) => resolved.push(waiter.handle) + }) + } + + // Why: amp's title never classifies, so this lane is its only rest signal; closing it for + // every launched agent failed `worker start` at agent_readiness after 60s (STA-7440). + it('settles an agent with no other rest signal once it has painted and gone quiet', async () => { + const resolved: string[] = [] + const foregroundReads: string[] = [] + const polls = createPolls('amp', resolved, foregroundReads) + const pty = makePty('pty-amp') + const leaf = makeLeaf('tab-amp') + polls.startPty(makeWaiter('pty'), pty) + polls.startLeaf(makeWaiter('leaf'), leaf) + + // Booting: the agent owns the foreground but has painted nothing (#9976). + await vi.advanceTimersByTimeAsync(INTERVAL_MS * 5) + expect(resolved).toEqual([]) + + await vi.advanceTimersByTimeAsync(INTERVAL_MS / 2) + pty.lastOutputAt = Date.now() + leaf.lastOutputAt = Date.now() + // The next sweep lands inside the quiet window measured from that paint. + await vi.advanceTimersByTimeAsync(INTERVAL_MS / 2) + expect(resolved).toEqual([]) + // A pane that cannot settle yet costs no process inspection. + expect(foregroundReads).toEqual([]) + + await vi.advanceTimersByTimeAsync(INTERVAL_MS) + expect(resolved).toEqual(['pty', 'leaf']) + expect(polls.activeTimerCount).toBe(0) + }) + + it('does not settle an agent that will announce rest itself on a quiet foreground', async () => { + const resolved: string[] = [] + const polls = createPolls('claude', resolved) + polls.startPty(makeWaiter('pty'), makePty('pty-claude', { lastOutputAt: Date.now() })) + + await vi.advanceTimersByTimeAsync(INTERVAL_MS * 5) + expect(resolved).toEqual([]) + }) +}) diff --git a/src/main/runtime/runtime-terminal-idle-polls.ts b/src/main/runtime/runtime-terminal-idle-polls.ts index 7306914af1f..d63b59974ee 100644 --- a/src/main/runtime/runtime-terminal-idle-polls.ts +++ b/src/main/runtime/runtime-terminal-idle-polls.ts @@ -15,21 +15,36 @@ import { evaluateTuiIdle, leafTuiIdleEvidence, ptyTuiIdleEvidence, + type QuietForegroundLane, type TuiIdleEvidenceSource, type TuiIdleVerdict } from './tui-idle-evidence' /** - * Why null counts as quiet: a record with no output timestamp has produced nothing the - * RUNTIME OBSERVED since it was created. That is not the same as silence — the reachable - * case is a daemon-hosted pane whose bytes never reach the runtime, which may still be - * streaming. The trade is deliberate: "never settles" becomes "settles uncorroborated", + * Why null counts as quiet on an `open` lane: a record with no output timestamp has produced + * nothing the RUNTIME OBSERVED since it was created. That is not the same as silence — the + * reachable case is a daemon-hosted pane whose bytes never reach the runtime, which may still + * be streaming. The trade is deliberate: "never settles" becomes "settles uncorroborated", * the caller keeps its timeout, and delivery cannot reach this lane. Reading it as `0ms since output` * inverted that — `0 >= quiescenceMs` is false forever, so an adopted pane that never * emitted could not settle no matter how long the caller waited. + * Why not on `after-paint`: that pane runs a known agent, whose TUI must paint before it can + * take input, so until the command has painted it is still booting. The shell's prompt and + * echoed command line are not the agent's paint (see terminal-command-paint.ts). */ -function isQuietForQuiescence(lastOutputAt: number | null, quiescenceMs: number): boolean { - return lastOutputAt === null ? true : Date.now() - lastOutputAt >= quiescenceMs +function isQuietForQuiescence( + lastOutputAt: number | null, + quiescenceMs: number, + lane: QuietForegroundLane, + commandPainted: () => boolean +): boolean { + if (lane === 'after-paint' && !commandPainted()) { + return false + } + if (lastOutputAt === null) { + return lane === 'open' + } + return Date.now() - lastOutputAt >= quiescenceMs } import type { TerminalWaiter } from './runtime-terminal-contracts' import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' @@ -37,6 +52,8 @@ import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-term type RuntimeTerminalIdlePollDependencies = TuiIdleEvidenceSource & { intervalMs: number getForegroundProcess(ptyId: string): Promise | null + /** Whether the pane's running command has painted anything of its own. */ + hasCommandPainted(ptyId: string): boolean /** The pane's rendered viewport, or null when the runtime holds no screen model for it. */ readVisibleScreen(ptyId: string): Promise | null /** Re-read the record the waiter registered against; see `sample` below. */ @@ -56,7 +73,7 @@ type IdlePollSample = { ptyId: string | null ready(): RuntimeTerminalWait blocked(reason: RuntimeTerminalWaitBlockedReason): RuntimeTerminalWait - isQuiet(): boolean + isQuiet(lane: QuietForegroundLane): boolean } const IDLE_ENTRY_FLAGS = { foregroundPollInFlight: false, screenReadInFlight: false } @@ -120,7 +137,10 @@ export class RuntimeTerminalIdlePolls { ptyId: pty.ptyId, ready: () => buildPtyTerminalWaitResult(handle, 'tui-idle', pty), blocked: (reason) => buildPtyTerminalWaitBlockedResult(handle, 'tui-idle', pty, reason), - isQuiet: () => isQuietForQuiescence(pty.lastOutputAt, this.deps.quiescenceMs) + isQuiet: (lane) => + isQuietForQuiescence(pty.lastOutputAt, this.deps.quiescenceMs, lane, () => + this.deps.hasCommandPainted(pty.ptyId) + ) } } // Why re-read: `syncWindowGraph` rebuilds `this.leaves` with fresh objects on every @@ -136,7 +156,13 @@ export class RuntimeTerminalIdlePolls { ptyId: leaf.ptyId, ready: () => buildTerminalWaitResult(handle, 'tui-idle', live()), blocked: (reason) => buildTerminalWaitBlockedResult(handle, 'tui-idle', live(), reason), - isQuiet: () => isQuietForQuiescence(live().lastOutputAt, this.deps.quiescenceMs) + isQuiet: (lane) => + isQuietForQuiescence( + live().lastOutputAt, + this.deps.quiescenceMs, + lane, + () => !leaf.ptyId || this.deps.hasCommandPainted(leaf.ptyId) + ) } } @@ -177,7 +203,10 @@ export class RuntimeTerminalIdlePolls { this.settle(entry, sample.ready()) return } - if (verdict.quietForeground && ptyId && !entry.foregroundPollInFlight) { + const lane = verdict.quietForeground + // Why quiet before the read too: a streaming or not-yet-painted pane cannot settle, so it + // must not pay a process inspection every tick for a whole turn. + if (lane !== 'closed' && ptyId && !entry.foregroundPollInFlight && sample.isQuiet(lane)) { const foregroundRead = this.deps.getForegroundProcess(ptyId) if (!foregroundRead) { return @@ -185,7 +214,7 @@ export class RuntimeTerminalIdlePolls { entry.foregroundPollInFlight = true startedForegroundPoll = true const foreground = await foregroundRead - if (foreground && !isShellProcess(foreground) && sample.isQuiet()) { + if (foreground && !isShellProcess(foreground) && sample.isQuiet(lane)) { this.settle(entry, sample.ready()) } } diff --git a/src/main/runtime/runtime-terminal-state-records.ts b/src/main/runtime/runtime-terminal-state-records.ts index 82e5124bf40..d030b896da9 100644 --- a/src/main/runtime/runtime-terminal-state-records.ts +++ b/src/main/runtime/runtime-terminal-state-records.ts @@ -11,6 +11,7 @@ import type { HeadlessEmulator } from '../daemon/headless-emulator' import type { PtyProviderBufferSnapshot } from '../providers/types' import type { RetainedTailRedrawCursor } from './terminal-tail-redraw-buffer' import type { TerminalTailWaitState } from './terminal-wait-tail-state' +import type { TerminalCommandPaint } from './terminal-command-paint' import type { PtyShellOwnershipMirror } from './pty-shell-ownership-mirror' import type { TerminalExitCause } from '../../shared/terminal-exit-cause' import type { AgentSessionOwnerBinding } from '../../shared/agent-session-host-authority' @@ -87,6 +88,8 @@ export type RuntimePtyWorktreeRecord = RuntimeTerminalTailState & { title: string | null titleUpdatedAt: number | null lastOutputAt: number | null + /** See terminal-command-paint.ts; absent until the pane's first output, and again after a gap or a new process. */ + commandPaint?: TerminalCommandPaint } export type RuntimePtyTabCloseAuthority = { diff --git a/src/main/runtime/runtime-worktree-agent-startup.test.ts b/src/main/runtime/runtime-worktree-agent-startup.test.ts index deb2eea281a..ba2c57b7854 100644 --- a/src/main/runtime/runtime-worktree-agent-startup.test.ts +++ b/src/main/runtime/runtime-worktree-agent-startup.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { Repo } from '../../shared/repo-types' +import { tuiAgentToAgentKind } from '../../shared/agent-kind' const mocks = vi.hoisted(() => ({ markAntigravityWorkspaceTrusted: vi.fn(), @@ -102,6 +103,22 @@ describe('buildWorktreeStartupForAgent host resolution', () => { request_kind: 'new' }) }) + + it('attributes a startup agent whose caller named no surface as unknown', () => { + const result = buildWorktreeStartupForAgent({ + repo: makeRepo({}), + settings, + agent: 'claude', + getLaunchPlatform: () => 'linux', + toSessionOptions: () => undefined + }) + + expect(result.startup.telemetry).toEqual({ + agent_kind: 'claude-code', + launch_source: 'unknown', + request_kind: 'new' + }) + }) }) describe('buildWorktreeStartupForDraft agent detection', () => { @@ -135,6 +152,34 @@ describe('buildWorktreeStartupForDraft agent detection', () => { expect(mocks.detectRemoteAgents).not.toHaveBeenCalled() expect(result?.agent).toBe('claude') }) + + // The host picks and launches this agent itself, so it is attributed like any other it builds, + // whether the draft rides the launch command or is pasted once the agent is up. + it.each([ + ['claude', 'cli', 'cli', false], + ['claude', undefined, 'unknown', false], + ['claude-agent-teams', 'orchestration', 'orchestration', true], + ['claude-agent-teams', undefined, 'unknown', true] + ] as const)( + 'attributes a %s draft launch named %s as %s', + async (agent, launchSource, expected, pasted) => { + const result = await buildWorktreeStartupForDraft({ + repo: makeRepo({}), + settings, + draft: 'ship it', + requestedAgent: agent, + getLaunchPlatform: () => 'linux', + ...(launchSource ? { launchSource } : {}) + }) + + expect(result?.draftPaste !== undefined).toBe(pasted) + expect(result?.startup.telemetry).toEqual({ + agent_kind: tuiAgentToAgentKind(agent), + launch_source: expected, + request_kind: 'new' + }) + } + ) }) describe('markLocalWorktreeTrusted', () => { diff --git a/src/main/runtime/runtime-worktree-agent-startup.ts b/src/main/runtime/runtime-worktree-agent-startup.ts index 10f335a4b05..faca0baf8fa 100644 --- a/src/main/runtime/runtime-worktree-agent-startup.ts +++ b/src/main/runtime/runtime-worktree-agent-startup.ts @@ -1,10 +1,9 @@ import { markQoderWorkspaceTrusted } from '../qoder/workspace-trust' +import { agentStartedTelemetry } from '../agent-launch/agent-started-telemetry' import type { AgentLaunchPreferences } from '../../shared/agent-session-host-authority' -import { tuiAgentToAgentKind } from '../../shared/agent-kind' import type { Repo } from '../../shared/repo-types' import type { TuiAgent } from '../../shared/tui-agent' import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' -import { launchSourceSchema } from '../../shared/telemetry-property-schemas' import { repoIsRemote } from '../../shared/agent-launch-remote' import { getRepoSshConnectionId } from '../../shared/execution-host' import { isTuiAgent, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' @@ -28,6 +27,11 @@ import type { RuntimeStore } from './runtime-store-contract' export type WorktreeStartupDraftPaste = { agent: TuiAgent; content: string } export type WorktreeStartupFollowup = { expectedProcess: string; prompt: string } +/** A fresh agent the host builds always carries its `agent_started` record; dropping it fails to compile. */ +type AttributedWorktreeStartupLaunch = WorktreeStartupLaunch & { + telemetry: NonNullable +} + type StartupEnvironment = { repo: Repo settings: ReturnType @@ -42,7 +46,7 @@ export async function buildWorktreeStartupForDraft( environment: StartupEnvironment & { draft: string; requestedAgent?: TuiAgent } ): Promise<{ agent: TuiAgent - startup: WorktreeStartupLaunch + startup: AttributedWorktreeStartupLaunch draftPaste?: WorktreeStartupDraftPaste } | null> { const content = environment.draft.trim() @@ -85,6 +89,7 @@ export async function buildWorktreeStartupForDraft( isRemote: repoIsRemote(repo), ...(environment.agentArgs !== undefined ? { agentArgs: environment.agentArgs } : {}) }) + const telemetry = agentStartedTelemetry(agent, environment.launchSource) const draftPlan = buildAgentDraftLaunchPlan({ ...launchArgs, draft: content }) if (draftPlan) { return { @@ -95,7 +100,8 @@ export async function buildWorktreeStartupForDraft( ...(draftPlan.startupCommandDelivery ? { startupCommandDelivery: draftPlan.startupCommandDelivery } : {}), - ...(draftPlan.env ? { env: draftPlan.env } : {}) + ...(draftPlan.env ? { env: draftPlan.env } : {}), + telemetry } } } @@ -115,7 +121,8 @@ export async function buildWorktreeStartupForDraft( ...(startupPlan.startupCommandDelivery ? { startupCommandDelivery: startupPlan.startupCommandDelivery } : {}), - ...(startupPlan.env ? { env: startupPlan.env } : {}) + ...(startupPlan.env ? { env: startupPlan.env } : {}), + telemetry }, draftPaste: { agent, content } } @@ -130,7 +137,11 @@ export function buildWorktreeStartupForAgent( preferences?: AgentLaunchPreferences ) => Parameters[0]['sessionOptions'] | undefined } -): { agent: TuiAgent; startup: WorktreeStartupLaunch; followup?: WorktreeStartupFollowup } { +): { + agent: TuiAgent + startup: AttributedWorktreeStartupLaunch + followup?: WorktreeStartupFollowup +} { const { agent, repo, settings } = environment if (!isTuiAgentEnabled(agent, settings.disabledTuiAgents)) { throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') @@ -150,7 +161,6 @@ export function buildWorktreeStartupForAgent( if (!startupPlan) { throw new Error(`Could not build launch command for ${agent}.`) } - const telemetry = agentLaunchTelemetry(agent, environment.launchSource) return { agent, startup: { @@ -160,7 +170,7 @@ export function buildWorktreeStartupForAgent( ? { startupCommandDelivery: startupPlan.startupCommandDelivery } : {}), ...(startupPlan.env ? { env: startupPlan.env } : {}), - ...(telemetry ? { telemetry } : {}) + telemetry: agentStartedTelemetry(agent, environment.launchSource) }, ...(startupPlan.followupPrompt ? { @@ -173,20 +183,6 @@ export function buildWorktreeStartupForAgent( } } -function agentLaunchTelemetry( - agent: TuiAgent, - launchSource: string | undefined -): WorktreeStartupLaunch['telemetry'] | undefined { - const parsed = launchSourceSchema.safeParse(launchSource) - return parsed.success - ? { - agent_kind: tuiAgentToAgentKind(agent), - launch_source: parsed.data, - request_kind: 'new' - } - : undefined -} - export async function markLocalWorktreeTrusted( agent: TuiAgent, workspacePath: string diff --git a/src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts b/src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts index 27d5ba48b29..a1373f43c71 100644 --- a/src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts +++ b/src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts @@ -89,7 +89,7 @@ async function awaitingApproval() { SESSION, { journal, - child: { phase: 'ready' as const }, + child: { phase: 'ready' as const, generation: 'child-1', fence: 1 }, params: { location: { executionHostId: 'local' as const, diff --git a/src/main/runtime/structured-agent-session-runtime.test.ts b/src/main/runtime/structured-agent-session-runtime.test.ts index 7749fc3c25b..9f4f4e2474d 100644 --- a/src/main/runtime/structured-agent-session-runtime.test.ts +++ b/src/main/runtime/structured-agent-session-runtime.test.ts @@ -205,36 +205,6 @@ describe('structured agent-session runtime install', () => { vi.restoreAllMocks() }) - it('starts orphan reaping and reports failures without failing installation', async () => { - stateDirectory = await mkdtemp(join(tmpdir(), 'orca-structured-runtime-')) - const failure = new Error('scan failed') - const reapOrphanChildren = vi.fn(async () => { - throw failure - }) - const onError = vi.fn() - - await expect( - ensureStructuredAgentSessionHost({ - stateDirectory, - hostId: HOST_ID, - claimKeyId: 'key-1', - resolveWorkspacePath: async () => stateDirectory!, - resolveClaudeAuthPolicy: () => ({ stripAuthEnv: true }), - resolveEnvironment: async () => ({}), - reapOrphanChildren, - onError - }) - ).resolves.toBeDefined() - - await vi.waitFor(() => - expect(onError).toHaveBeenCalledWith({ - scope: 'agent-session-orphan-child-reaper', - error: failure - }) - ) - expect(reapOrphanChildren).toHaveBeenCalledWith({ store: expect.anything() }) - }) - it('holds stop until the model catalog has written its coalesced save', async () => { stateDirectory = await mkdtemp(join(tmpdir(), 'orca-structured-runtime-')) await ensureStructuredAgentSessionHost({ @@ -243,8 +213,7 @@ describe('structured agent-session runtime install', () => { claimKeyId: 'key-1', resolveWorkspacePath: async () => stateDirectory!, resolveClaudeAuthPolicy: () => ({ stripAuthEnv: true }), - resolveEnvironment: async () => ({}), - reapOrphanChildren: async () => [] + resolveEnvironment: async () => ({}) }) let finishWrite = (): void => {} vi.spyOn(agentModelCatalogStore, 'flushPersistence').mockReturnValue( @@ -265,33 +234,6 @@ describe('structured agent-session runtime install', () => { expect(stopped).toBe(true) }) - it('logs an orphan-reaper failure when no reporter is configured', async () => { - stateDirectory = await mkdtemp(join(tmpdir(), 'orca-structured-runtime-')) - const failure = new Error('scan failed') - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - - await expect( - ensureStructuredAgentSessionHost({ - stateDirectory, - hostId: HOST_ID, - claimKeyId: 'key-1', - resolveWorkspacePath: async () => stateDirectory!, - resolveClaudeAuthPolicy: () => ({ stripAuthEnv: true }), - resolveEnvironment: async () => ({}), - reapOrphanChildren: async () => { - throw failure - } - }) - ).resolves.toBeDefined() - - await vi.waitFor(() => - expect(consoleError).toHaveBeenCalledWith( - '[structured-agent-session] orphan reaper failed', - failure - ) - ) - }) - it('does not infer Windows process identity support from an injected reader', async () => { stateDirectory = await mkdtemp(join(tmpdir(), 'orca-structured-runtime-')) const originalPlatform = process.platform @@ -355,7 +297,6 @@ describe('a teardown that fails is retried by the next stop', () => { claimKeyId: 'key-1', resolveWorkspacePath: async () => directory!, resolveEnvironment: async () => ({}), - reapOrphanChildren: async () => [], resolveClaudeAuthPolicy: () => ({ stripAuthEnv: true }) }) diff --git a/src/main/runtime/structured-agent-session-runtime.ts b/src/main/runtime/structured-agent-session-runtime.ts index f4a1d1b8185..1659fcd512e 100644 --- a/src/main/runtime/structured-agent-session-runtime.ts +++ b/src/main/runtime/structured-agent-session-runtime.ts @@ -38,7 +38,6 @@ import { } from '../native-chat/claude-structured-managed-account-support' import { AgentSessionRecordStore } from './agent-session-record-store' import { agentSessionStorePath } from './agent-session-record-store-file' -import { stopOrphanAgentSessionChildren } from './agent-session-orphan-child-reaper' import { createStructuredAgentSessionOwnerProbe, createStructuredAgentSessionOwnerProbes @@ -106,7 +105,6 @@ export type StructuredAgentSessionRuntimeDeps = { statusSink?: StructuredAgentSessionHostDeps['statusSink'] /** See `StructuredAgentSessionHostDeps.hasOpenDispatch`. */ hasOpenDispatch?: StructuredAgentSessionHostDeps['hasOpenDispatch'] - reapOrphanChildren?: typeof stopOrphanAgentSessionChildren /** The account home a structured launch would pin right now, for catalog * reads with no session record. Absent disables the catalog surface. */ resolveAgentAccountHome?: RuntimeAgentAccountHomeResolver @@ -199,21 +197,6 @@ async function install(deps: StructuredAgentSessionRuntimeDeps): Promise { - try { - if (deps.onError) { - deps.onError({ scope: 'agent-session-orphan-child-reaper', error }) - } else { - console.error('[structured-agent-session] orphan reaper failed', error) - } - } catch (reportingError) { - console.error( - '[structured-agent-session] orphan reaper error reporting failed', - reportingError - ) - } - }) let host: StructuredAgentSessionHost | null = null const lifecycle = createStructuredAgentSessionLifecycleDelivery({ handle: (event) => host?.handleAdapterEvent(event), diff --git a/src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts b/src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts new file mode 100644 index 00000000000..d0650d94f49 --- /dev/null +++ b/src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts @@ -0,0 +1,191 @@ +// The spawn token is an environment variable, so every descendant of a provider child inherits it: +// an editor the agent opened with `code .`, a tmux server, a detached dev server. Carrying the token +// proves descent, not being the provider child, so the host never signals a process on the token +// alone. The token scan still answers one question soundly: whether a reservation ever spawned. + +import type * as NodeFsPromises from 'node:fs/promises' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { CODEX_SPAWN_TOKEN_ENV } from '../codex/codex-structured-owner-identity' +import { AgentSessionRecordStore } from './agent-session-record-store' +import type { AgentSessionReserveRequest } from './agent-session-reservation-admission' +import { createStructuredAgentSessionOwnerProbe } from './structured-agent-session-owner-probe' +import { + ensureStructuredAgentSessionHost, + stopStructuredAgentSessionRuntime +} from './structured-agent-session-runtime' + +/** pid -> the NUL-separated environment block `/proc//environ` serves. */ +const fakeProc = vi.hoisted(() => ({ environs: new Map() })) + +vi.mock('node:fs/promises', async (importOriginal) => { + const fsp = await importOriginal() + const enoent = (path: string) => Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' }) + return { + ...fsp, + readdir: async (...args: Parameters) => + args[0] === '/proc' ? [...fakeProc.environs.keys()].map(String) : fsp.readdir(...args), + readFile: async (...args: Parameters) => { + const path = String(args[0]) + if (!path.startsWith('/proc/')) { + return fsp.readFile(...args) + } + const environ = fakeProc.environs.get(Number(path.split('/')[2])) + if (environ === undefined || !path.endsWith('/environ')) { + throw enoent(path) + } + return environ + } + } +}) + +const HOST_ID = 'local' +const NOW = 1_800_000_000_000 +const SESSION = 'session-alpha' +const MINTED_TOKEN = 'spawn-minted-by-this-store' + +function environ(token: string | null): string { + return ['PATH=/usr/bin', ...(token ? [`${CODEX_SPAWN_TOKEN_ENV}=${token}`] : [])].join('\0') +} + +function reserveRequest(): AgentSessionReserveRequest { + const operationId = `${NOW}-${'1'.padStart(32, '0')}` + return { + sessionId: SESSION, + location: { + executionHostId: HOST_ID, + wslDistro: null, + workspaceId: 'workspace-1', + workspaceKind: 'git-worktree' + }, + provider: 'codex', + accountHome: { variable: 'CODEX_HOME', path: '/home/dev/.codex' }, + expectedFence: null, + spawnToken: MINTED_TOKEN, + claimKeyId: 'key-1', + handoffOperationId: null, + probe: { outcome: 'indeterminate', reason: 'no previous owner' }, + operation: { callerKey: 'client-1', operationId, fingerprint: 'fp-1' }, + now: NOW + } +} + +const originalPlatform = process.platform +let stateDirectory: string + +beforeEach(async () => { + stateDirectory = await mkdtemp(join(tmpdir(), 'orca-spawn-token-descendants-')) + Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }) +}) + +afterEach(async () => { + await stopStructuredAgentSessionRuntime() + Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform }) + fakeProc.environs.clear() + vi.restoreAllMocks() + await rm(stateDirectory, { recursive: true, force: true }) +}) + +function openStore(): Promise { + return AgentSessionRecordStore.open({ + directory: join(stateDirectory, 'agent-sessions'), + hostId: HOST_ID + }) +} + +describe('a process that inherited a spawn token', () => { + it('is never signalled when the host installs and reconciles, even when this store minted the token', async () => { + // A chat ran and ended: its root exited and the lease was released, clearing the token. + const seed = await openStore() + const reserved = await seed.reserveOwner(reserveRequest()) + const fence = reserved.record.lease.runtimeFence + await seed.commitProcessIdentity({ + sessionId: SESSION, + fence, + process: { hostId: HOST_ID, pid: 4242, processStartTimeMs: NOW, spawnToken: MINTED_TOKEN }, + now: NOW + }) + await seed.proveOwner({ + sessionId: SESSION, + fence, + link: { + linkId: 'link-1', + handle: { provider: 'codex', threadId: 'thread-1' }, + origin: 'created', + mintedAtFence: fence, + observedAt: NOW + }, + now: NOW + }) + const released = await seed.evictProvenDeadOwner({ + sessionId: SESSION, + expectedFence: fence, + probe: { outcome: 'pid-absent' }, + now: NOW + }) + expect(released.lease).toMatchObject({ claimStatus: 'released', reservedSpawnToken: null }) + + // The editor the agent opened detached into its own group and outlived the chat, still + // carrying the token; another descendant carries a token no store here ever minted. + fakeProc.environs.set(1, environ(null)) + fakeProc.environs.set(5151, environ(MINTED_TOKEN)) + fakeProc.environs.set(6161, environ('spawn-foreign')) + const kill = vi.spyOn(process, 'kill').mockImplementation(() => true) + + const host = await ensureStructuredAgentSessionHost({ + stateDirectory, + hostId: HOST_ID, + claimKeyId: 'key-1', + resolveWorkspacePath: async () => stateDirectory, + resolveClaudeAuthPolicy: () => ({ stripAuthEnv: true }), + resolveEnvironment: async () => ({}) + }) + await host.reconcileRestartLeases() + // Install work that is not awaited would have run by now; nothing here waits on a timer. + await new Promise((resolve) => setTimeout(resolve, 50)) + + expect(kill.mock.calls.filter(([, signal]) => signal !== 0)).toEqual([]) + }) +}) + +describe('the reservation owner probe', () => { + async function reconcileReservation(): Promise { + const crashed = await openStore() + await crashed.reserveOwner(reserveRequest()) + // The restart after a crash between reservation and recorded identity. + const store = await openStore() + await store.reconcileOnRestart({ + probe: createStructuredAgentSessionOwnerProbe(HOST_ID), + now: NOW + }) + return store + } + + it('proves a reservation never spawned when the host scan finds no process carrying its token', async () => { + fakeProc.environs.set(1, environ(null)) + fakeProc.environs.set(6161, environ('spawn-foreign')) + + const store = await reconcileReservation() + + expect(store.getRecord(SESSION)?.lease).toMatchObject({ + claimStatus: 'released', + deathEvidence: { kind: 'pid-absent', detail: 'reservation never spawned' } + }) + }) + + it('claims no absence when the host scan finds a process carrying the token', async () => { + fakeProc.environs.set(5151, environ(MINTED_TOKEN)) + const kill = vi.spyOn(process, 'kill').mockImplementation(() => true) + + const store = await reconcileReservation() + + // Released without evidence, and the process carrying the token is left alone. + expect(store.getRecord(SESSION)?.lease).toMatchObject({ + claimStatus: 'released', + deathEvidence: null + }) + expect(kill.mock.calls.filter(([, signal]) => signal !== 0)).toEqual([]) + }) +}) diff --git a/src/main/runtime/structured-agent-session-support-probe.test.ts b/src/main/runtime/structured-agent-session-support-probe.test.ts index 07a2b671f0a..ea0ab3298d9 100644 --- a/src/main/runtime/structured-agent-session-support-probe.test.ts +++ b/src/main/runtime/structured-agent-session-support-probe.test.ts @@ -22,19 +22,15 @@ function setPlatform(platform: NodeJS.Platform): void { type InstallEffects = { storeOpened: boolean - reaperStarted: boolean } -/** Stands in for `install()` by performing the two effects it performs, so a probe that +/** Stands in for `install()` by performing the effect it performs, so a probe that * reinstalls the host is caught by what the install *does*, not by a call count alone. */ function stubStructuredHostInstall(runtime: OrcaRuntimeService): { effects: InstallEffects ensure: ReturnType } { - const effects: InstallEffects = { - storeOpened: false, - reaperStarted: false - } + const effects: InstallEffects = { storeOpened: false } // `supportsCreate` answers as the real Codex adapter would, so a probe that reinstalls the host // still returns the right answer and fails on the install effects alone. const host = { @@ -44,7 +40,6 @@ function stubStructuredHostInstall(runtime: OrcaRuntimeService): { } const ensure = vi.fn(async () => { effects.storeOpened = true - effects.reaperStarted = true setStructuredAgentSessionHost(host as never) }) vi.spyOn(runtime, 'ensureStructuredAgentSessionHost').mockImplementation(ensure) @@ -94,10 +89,7 @@ async function expectSupportWithoutInstall(input: { expect(answers).toEqual(Array(input.repetitions ?? 1).fill(input.expected)) expect(ensure).not.toHaveBeenCalled() - expect(effects).toEqual({ - storeOpened: false, - reaperStarted: false - }) + expect(effects).toEqual({ storeOpened: false }) expect(getStructuredAgentSessionHost()).toBeNull() } @@ -193,10 +185,7 @@ describe('structured agent-session create-support probe', () => { await runtime.prepareStructuredAgentSessionStartupRestoration() expect(ensure).toHaveBeenCalledTimes(1) - expect(effects).toEqual({ - storeOpened: true, - reaperStarted: true - }) + expect(effects).toEqual({ storeOpened: true }) expect( (getStructuredAgentSessionHost() as unknown as { reconcileRestartLeases: () => void }) .reconcileRestartLeases diff --git a/src/main/runtime/structured-worker-terminal-read.ts b/src/main/runtime/structured-worker-terminal-read.ts index 3d31c4c863c..60dd1126067 100644 --- a/src/main/runtime/structured-worker-terminal-read.ts +++ b/src/main/runtime/structured-worker-terminal-read.ts @@ -29,7 +29,7 @@ */ import type { RuntimeTerminalRead } from '../../shared/runtime-types' -import { formatWorkerTranscriptMessage } from '../../shared/worker-transcript-text' +import { formatWorkerTranscriptMessages } from '../../shared/worker-transcript-text' import { AGENT_SESSION_NOT_ATTACHED } from '../native-chat/agent-session-wire/structured-agent-session-mutation-admission' import type { OrchestrationDb } from './orchestration/db' import { boundStructuredJournalTail } from './orchestration/structured-worker-journal-archive' @@ -84,9 +84,7 @@ export async function readStructuredWorkerTerminal(args: { } // Redacts dispatch capabilities and clips oversized blocks under the archive path's byte bound. const bounded = boundStructuredJournalTail(page.items) - const lines = bounded.messages.flatMap((message) => - formatWorkerTranscriptMessage(message).split('\n') - ) + const lines = formatWorkerTranscriptMessages(bounded.messages).flatMap((text) => text.split('\n')) const read = readTerminalTail({ handle: args.handle, status: structuredWorkerTerminalState(observeStructuredWorker(identity).status), diff --git a/src/main/runtime/terminal-command-paint.test.ts b/src/main/runtime/terminal-command-paint.test.ts new file mode 100644 index 00000000000..54493da686f --- /dev/null +++ b/src/main/runtime/terminal-command-paint.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' +import { + hasTerminalCommandPainted, + observeTerminalCommandPaint, + type TerminalCommandPaintRecord +} from './terminal-command-paint' +import { normalizeTerminalChunk } from './terminal-ansi-normalization' + +function observe(record: TerminalCommandPaintRecord, data: string): void { + observeTerminalCommandPaint(record, data, normalizeTerminalChunk(data).text) +} + +describe('terminal command paint', () => { + it('reads a pane with no command-start marker as painted', () => { + const record: TerminalCommandPaintRecord = {} + observe(record, '~/repo % ') + expect(hasTerminalCommandPainted(record)).toBe(true) + }) + + it('does not count the prompt, the echoed command, or a title set after the marker', () => { + const record: TerminalCommandPaintRecord = {} + observe(record, '\x1b]133;A\x07~/repo % amp\r\n\x1b]133;C\x07\x1b]0;amp\x07') + expect(hasTerminalCommandPainted(record)).toBe(false) + observe(record, '\x1b[?1049h\x1b[?25l\r\n') + expect(hasTerminalCommandPainted(record)).toBe(false) + observe(record, '\x1b[2;3H╭─ Amp') + expect(hasTerminalCommandPainted(record)).toBe(true) + }) + + it('counts a paint that arrives in the same chunk as the marker', () => { + const record: TerminalCommandPaintRecord = {} + observe(record, '\x1b]133;C\x1b\\\x1b[?1049hLIVE-TUI') + expect(hasTerminalCommandPainted(record)).toBe(true) + }) + + it('finds a marker split across chunks, and counts only output after it', () => { + const record: TerminalCommandPaintRecord = {} + observe(record, '~/repo % amp\r\n\x1b]13') + observe(record, '3;C\x07') + expect(hasTerminalCommandPainted(record)).toBe(false) + observe(record, '\x1b]133;C;cmdline_url=') + observe(record, 'amp\x07╭─ Amp') + expect(hasTerminalCommandPainted(record)).toBe(true) + }) + + it('starts over at the next command', () => { + const record: TerminalCommandPaintRecord = {} + observe(record, '\x1b]133;C\x07goose> ') + observe(record, '\x1b]133;D;0\x07\x1b]133;A\x07% goose\r\n\x1b]133;C\x07') + expect(hasTerminalCommandPainted(record)).toBe(false) + }) +}) diff --git a/src/main/runtime/terminal-command-paint.ts b/src/main/runtime/terminal-command-paint.ts new file mode 100644 index 00000000000..d415d52dae7 --- /dev/null +++ b/src/main/runtime/terminal-command-paint.ts @@ -0,0 +1,66 @@ +import { stripAnsiEscapeSequences } from '../../shared/ansi-escape-sequences' +import { createOsc133CommandFinishedScanner } from '../../shared/terminal-osc133-command-finished' +import { normalizeTerminalChunk } from './terminal-ansi-normalization' + +// eslint-disable-next-line no-control-regex -- control bytes are exactly what is not visible. +const VISIBLE_CHARACTER_RE = /[^\s\u0000-\u001f\u007f-\u009f]/ + +function hasVisibleText(normalizedText: string): boolean { + return VISIBLE_CHARACTER_RE.test(stripAnsiEscapeSequences(normalizedText)) +} + +/** + * Where the running command's own output begins. Orca's shell integration prints OSC 133;C + * after the prompt and the echoed command line, right before the command runs, so only + * visible output after it can be the command's. + */ +export class TerminalCommandPaint { + private awaitingPaint = false + private markerEndInChunk = -1 + private readonly scanner = createOsc133CommandFinishedScanner( + () => {}, + (endInChunk) => { + this.markerEndInChunk = endInChunk + } + ) + + observe(data: string, normalizedText: string): void { + this.markerEndInChunk = -1 + this.scanner.scan(data) + if (this.markerEndInChunk !== -1) { + // Why the rest of this chunk counts: a fast binary can paint in the same read as the marker. + this.awaitingPaint = !hasVisibleText( + normalizeTerminalChunk(data.slice(this.markerEndInChunk)).text + ) + return + } + if (this.awaitingPaint && hasVisibleText(normalizedText)) { + this.awaitingPaint = false + } + } + + hasPainted(): boolean { + return !this.awaitingPaint + } +} + +export type TerminalCommandPaintRecord = { commandPaint?: TerminalCommandPaint } + +/** `normalizedText` is the chunk as the tail buffer received it (pending escapes resolved). */ +export function observeTerminalCommandPaint( + record: TerminalCommandPaintRecord, + data: string, + normalizedText: string +): void { + record.commandPaint ??= new TerminalCommandPaint() + record.commandPaint.observe(data, normalizedText) +} + +/** + * Why no marker reads as painted: some launches get no command boundary (older fish, SSH relay + * zsh, shells Orca could not wrap, and Windows launches that pass the command in shell args), so + * any output is all the evidence there is. + */ +export function hasTerminalCommandPainted(record: TerminalCommandPaintRecord): boolean { + return record.commandPaint?.hasPainted() ?? true +} diff --git a/src/main/runtime/terminal-retirement-async-durability.test.ts b/src/main/runtime/terminal-retirement-async-durability.test.ts index 038832782d7..28d2f1603a9 100644 --- a/src/main/runtime/terminal-retirement-async-durability.test.ts +++ b/src/main/runtime/terminal-retirement-async-durability.test.ts @@ -2,6 +2,7 @@ import { afterEach, expect, it, vi } from 'vitest' import { ACK_INCARNATION, ACK_LEAF, + ACK_SECOND_LEAF, ACK_TAB, createAcknowledgedTabRetirementFixture } from './acknowledged-terminal-tab-retirement-fixture' @@ -35,7 +36,7 @@ it('withholds the acknowledged close until its host retirement is durable', asyn expect(f.hasTab()).toBe(false) }) -it('publishes physical-exit retirement only after durability', async () => { +it('publishes physical-exit retirement before its durable write completes', async () => { const f = fixture() await f.store.flushPendingOrThrowAsync() const published = vi.fn() @@ -43,7 +44,8 @@ it('publishes physical-exit retirement only after durability', async () => { const gate = f.authority.pause() const exiting = f.runtime.onPtyExit('pty-a', 0, ACK_INCARNATION, { providerExitObserved: true }) await gate.started.promise - expect(published).not.toHaveBeenCalled() + // Why: a client re-activating the exited pane in this window must already find it retired. + expect(published).toHaveBeenCalled() gate.finish.resolve() await exiting expect(published).toHaveBeenCalled() @@ -53,7 +55,7 @@ it('publishes physical-exit retirement only after durability', async () => { unsubscribe() }) -it('does not publish a delayed exit over a newly admitted incarnation', async () => { +it('publishes nothing when the exit write lands after a replacement is admitted', async () => { const f = fixture() await f.store.flushPendingOrThrowAsync() const published = vi.fn() @@ -68,3 +70,58 @@ it('does not publish a delayed exit over a newly admitted incarnation', async () expect(published).not.toHaveBeenCalled() unsubscribe() }) + +async function exitWithFailedDurableWrite(f: ReturnType): Promise { + await f.store.flushPendingOrThrowAsync() + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) + f.authority.failNextWrite() + await f.runtime.onPtyExit('pty-a', 0, ACK_INCARNATION, { providerExitObserved: true }) + expect(errorSpy).toHaveBeenCalledWith( + '[runtime] terminal retirement is not yet durable:', + expect.any(Error) + ) + errorSpy.mockRestore() + // The failed write left disk untouched, so a relaunch would still load the exited leaf. + expect(f.readDisk().workspaceSession.terminalLayoutsByTabId[ACK_TAB]?.ptyIdsByLeafId).toEqual({ + [ACK_LEAF]: 'pty-a', + [ACK_SECOND_LEAF]: 'pty-b' + }) +} + +it('persists a failed exit retirement with the next unrelated profile write', async () => { + const f = fixture() + await exitWithFailedDurableWrite(f) + f.store.addRepo({ + id: 'repo2', + path: '/tmp/other', + displayName: 'Other', + badgeColor: 'gray', + addedAt: 2 + }) + await f.store.flushPendingOrThrowAsync() + expect(f.readDisk().workspaceSession.terminalLayoutsByTabId[ACK_TAB]?.ptyIdsByLeafId).toEqual({ + [ACK_SECOND_LEAF]: 'pty-b' + }) +}) + +it('persists a failed exit retirement with the final quit flush', async () => { + const f = fixture() + await exitWithFailedDurableWrite(f) + await f.quit() + expect(f.readDisk().workspaceSession.terminalLayoutsByTabId[ACK_TAB]?.ptyIdsByLeafId).toEqual({ + [ACK_SECOND_LEAF]: 'pty-b' + }) +}) + +it('writes exits retired in the same tick once', async () => { + const f = fixture() + await f.store.flushPendingOrThrowAsync() + const writes = f.authority.captures.length + await Promise.all([ + f.runtime.onPtyExit('pty-a', 0, ACK_INCARNATION, { providerExitObserved: true }), + f.runtime.onPtyExit('pty-b', 0, undefined, { providerExitObserved: true }) + ]) + // Why: the first write already carries both retirements; the second has nothing left to fence. + expect(f.authority.captures.length - writes).toBe(1) + expect(f.readDisk().workspaceSession.terminalLayoutsByTabId[ACK_TAB]).toBeUndefined() +}) diff --git a/src/main/runtime/terminal-wait-detection.ts b/src/main/runtime/terminal-wait-detection.ts index bb102a3e7b8..1540eef2d5f 100644 --- a/src/main/runtime/terminal-wait-detection.ts +++ b/src/main/runtime/terminal-wait-detection.ts @@ -8,6 +8,10 @@ import { import type { RuntimeTerminalWaitBlockedReason } from '../../shared/runtime-types' import type { TuiAgent } from '../../shared/tui-agent' import { findAntigravityReadyPromptIndex } from './antigravity-terminal-readiness' +import { + findCodexScreenReadyPromptIndex, + isCodexComposerReadyScreen +} from './codex-terminal-readiness' import { startOfLastLines, startOfLastNonBlankLines } from './terminal-wait-tail-window' const EXPLICIT_IDLE_TITLE_RE = /(^|\s)(ready|idle|done)(\s|$|[.!?])/i @@ -72,12 +76,29 @@ export function isKnownReadyPromptBody( if (agent !== null && agent !== 'codex') { return false } - const screenLines = readScreenLines() - if (screenLines === null) { - return false + const screen = readScreen(readScreenLines) + return screen !== null && isReadyPromptUnblocked(screen, findCodexScreenReadyPromptIndex(screen)) +} + +/** + * Tier 1b body evidence: a ready screen from an agent with no title rest signal. Unlike tier 1 + * it only proves the TUI is up, so the ranking holds it to quiescence. + * Why codex panes only: a `cat`ed transcript or pager in an unknown pane can show the composer. + */ +export function isQuietReadyScreenBody( + waitText: string, + agent: TuiAgent | null, + readScreenLines: () => readonly string[] | null +): boolean { + if (agent === 'codex') { + const screen = readScreen(readScreenLines) + return screen !== null && isCodexComposerReadyScreen(screen) } - const screen = screenLines.join('\n').toLowerCase() - return isReadyPromptUnblocked(screen, findCodexScreenReadyPromptIndex(screen)) + return (agent === null || agent === 'muse') && isMuseReadyPromptPreview(waitText) +} + +function readScreen(readScreenLines: () => readonly string[] | null): string | null { + return readScreenLines()?.join('\n').toLowerCase() ?? null } function isReadyPromptUnblocked(normalized: string, readyIndex: number | null): boolean { @@ -88,8 +109,6 @@ function isReadyPromptUnblocked(normalized: string, readyIndex: number | null): return blockedSignal === null || blockedSignal.index <= readyIndex } -// Why separate from isKnownReadyPromptPreview: that one settles tier 1 immediately, while -// a Muse ready screen only proves the TUI is up — the ranking holds it to quiescence. export function isMuseReadyPromptPreview(preview: string): boolean { const normalized = preview.toLowerCase() return isReadyPromptUnblocked(normalized, findMuseReadyPromptIndex(normalized)) @@ -179,24 +198,6 @@ function findCodexReadyPromptIndex(normalized: string): number | null { return readySegment.includes('model:') && readySegment.includes('directory:') ? headerIndex : null } -const CODEX_HEADER_LOADING_RE = /(?:model|directory):\s+loading/ - -// Why the header box only: chat below it can mention "OpenAI Codex" or `model: loading`. -// Why `loading`: a header still loading is not ready; the screen must not add readiness early. -function findCodexScreenReadyPromptIndex(screen: string): number | null { - const headerIndex = screen.indexOf('openai codex') - if (headerIndex === -1) { - return null - } - const boxEnd = screen.indexOf('╰', headerIndex) - const header = screen.slice(headerIndex, boxEnd === -1 ? undefined : boxEnd) - return header.includes('model:') && - header.includes('directory:') && - !CODEX_HEADER_LOADING_RE.test(header) - ? headerIndex - : null -} - export const TERMINAL_WAIT_BLOCKED_SENTINEL_RE = /update available|choose working directory to|codex just got an upgrade|hooks need review|do you trust|trust this|trusted workspace|press enter to (?:confirm|continue|view|insert)|press t to trust|permission required|requires permission|allow once|allow always|run this command\?/i diff --git a/src/main/runtime/terminal-wait-name-only-idle.test.ts b/src/main/runtime/terminal-wait-name-only-idle.test.ts index 3ad2e74d2e6..cf4648bf353 100644 --- a/src/main/runtime/terminal-wait-name-only-idle.test.ts +++ b/src/main/runtime/terminal-wait-name-only-idle.test.ts @@ -59,6 +59,7 @@ function createWait(options: { ...shared, intervalMs: POLL_INTERVAL_MS, getForegroundProcess: () => Promise.resolve(options.foreground ?? null), + hasCommandPainted: () => true, getLiveLeaf: (leaf) => options.liveLeaf?.() ?? leaf, resolve: (waiter, result) => waiters.resolve(waiter, result) }) diff --git a/src/main/runtime/tui-idle-command-paint.test.ts b/src/main/runtime/tui-idle-command-paint.test.ts new file mode 100644 index 00000000000..fda31e06ebb --- /dev/null +++ b/src/main/runtime/tui-idle-command-paint.test.ts @@ -0,0 +1,57 @@ +/** + * An agent with no rest signal of its own (amp) settles only on a quiet foreground, and only + * once it has painted. The shell's prompt and the echoed launch command land first, so the + * runtime must tell them apart from the agent's paint by the shell's command-start marker. + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createTranscriptPane, TRANSCRIPT_PANE_PTY_ID } from './agent-transcript-pane-test-harness' + +const POLL_INTERVAL_MS = 2_000 + +describe('tui-idle on an agent with no rest signal', () => { + afterEach(() => { + vi.useRealTimers() + }) + + it('waits through a silent boot after the shell echoed the command, then settles on the paint', async () => { + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'Terminal', + foregroundProcess: 'amp', + launchAgent: 'amp', + data: '' + }) + vi.useFakeTimers() + const write = (chunk: string) => runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, chunk, Date.now()) + write('\x1b]133;A\x07~/repo % amp\r\n\x1b]133;C\x07\x1b]0;amp\x07') + const settled = vi.fn() + const wait = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 60_000 }) + wait.then(settled, () => {}) + + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 5) + expect(settled).not.toHaveBeenCalled() + + write('\x1b[?1049h\x1b[H╭─ Amp ─╮\r\n│ > │') + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 3) + await expect(wait).resolves.toMatchObject({ satisfied: true }) + }) + + it('does not finish a command-start marker cut off by dropped output', async () => { + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'Terminal', + foregroundProcess: 'amp', + launchAgent: 'amp', + data: '' + }) + vi.useFakeTimers() + const write = (chunk: string) => runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, chunk, Date.now()) + // Fish's marker carries the command line, so a cut-off one can terminate on unrelated output. + write('~/repo % amp\r\n\x1b]133;C;cmdline_url=am') + runtime.notePtyDataGap(TRANSCRIPT_PANE_PTY_ID, 4096) + write('╭─ Amp ─╮\r\n│ > │\x1b]0;amp\x07') + const wait = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 60_000 }) + + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 3) + await expect(wait).resolves.toMatchObject({ satisfied: true }) + }) +}) diff --git a/src/main/runtime/tui-idle-evidence.test.ts b/src/main/runtime/tui-idle-evidence.test.ts index ec88c3368a3..ee8e908c61b 100644 --- a/src/main/runtime/tui-idle-evidence.test.ts +++ b/src/main/runtime/tui-idle-evidence.test.ts @@ -1,8 +1,14 @@ import { describe, expect, it } from 'vitest' +import { detectAgentStatusFromTitle } from '../../shared/agent-title-status' +import { getSyntheticAgentTerminalTitle } from '../../shared/synthetic-agent-title' +import { isTuiAgent, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' +import { getTuiAgentRestSignal } from '../../shared/tui-agent-rest-signal' +import { isKnownReadyPromptBody } from './terminal-wait-detection' import { evaluateTuiIdle, + hasFreshDoneFirstPartyStatus, + hasQuietReadyScreen, isTuiIdleReadyVerdict, - hasQuietMuseReadyPrompt, nameOnlyIdleNeedsCorroboration, type TuiIdleEvaluationInput, type TuiIdleEvidenceRecord @@ -24,7 +30,7 @@ function input(overrides: Partial = {}): TuiIdleEvaluati record: record(), readTailBlockedReason: () => null, readPositiveBodyEvidence: () => false, - readMuseReadyBodyEvidence: () => true, + readQuietReadyBodyEvidence: () => true, agent: 'muse', firstPartyStatus: null, quiescenceMs: QUIESCENCE_MS, @@ -32,39 +38,38 @@ function input(overrides: Partial = {}): TuiIdleEvaluati } } -describe('hasQuietMuseReadyPrompt', () => { +describe('hasQuietReadyScreen', () => { it('settles a Muse ready screen once the stream has gone quiet', () => { - expect(hasQuietMuseReadyPrompt(record(), 'muse', () => true, QUIESCENCE_MS)).toBe(true) + expect(hasQuietReadyScreen(record(), 'muse', () => true, QUIESCENCE_MS)).toBe(true) }) it('refuses while the pane is still streaming', () => { expect( - hasQuietMuseReadyPrompt( - record({ lastOutputAt: Date.now() }), - 'muse', - () => true, - QUIESCENCE_MS - ) + hasQuietReadyScreen(record({ lastOutputAt: Date.now() }), 'muse', () => true, QUIESCENCE_MS) ).toBe(false) }) it('refuses without an output clock, like the tier-3 lane', () => { expect( - hasQuietMuseReadyPrompt(record({ lastOutputAt: null }), 'muse', () => true, QUIESCENCE_MS) + hasQuietReadyScreen(record({ lastOutputAt: null }), 'muse', () => true, QUIESCENCE_MS) ).toBe(false) }) it('refuses without a ready screen', () => { - expect(hasQuietMuseReadyPrompt(record(), 'muse', () => false, QUIESCENCE_MS)).toBe(false) + expect(hasQuietReadyScreen(record(), 'muse', () => false, QUIESCENCE_MS)).toBe(false) }) it('covers adopted panes that carry no launch metadata', () => { - expect(hasQuietMuseReadyPrompt(record(), null, () => true, QUIESCENCE_MS)).toBe(true) - expect(hasQuietMuseReadyPrompt(record(), undefined, () => true, QUIESCENCE_MS)).toBe(true) + expect(hasQuietReadyScreen(record(), null, () => true, QUIESCENCE_MS)).toBe(true) + expect(hasQuietReadyScreen(record(), undefined, () => true, QUIESCENCE_MS)).toBe(true) }) - it('refuses another agent quoting Muse in its scrollback', () => { - expect(hasQuietMuseReadyPrompt(record(), 'codex', () => true, QUIESCENCE_MS)).toBe(false) + it('covers Codex, whose title carries no rest signal once idle', () => { + expect(hasQuietReadyScreen(record(), 'codex', () => true, QUIESCENCE_MS)).toBe(true) + }) + + it('refuses another agent quoting Muse or Codex in its scrollback', () => { + expect(hasQuietReadyScreen(record(), 'claude', () => true, QUIESCENCE_MS)).toBe(false) }) }) @@ -81,7 +86,7 @@ describe('evaluateTuiIdle muse lane', () => { }) describe('evaluateTuiIdle ranking', () => { - const noMuse = { readMuseReadyBodyEvidence: () => false } + const noMuse = { readQuietReadyBodyEvidence: () => false } it('ranks a blocking prompt in the tail above an explicit idle title', () => { const verdict = evaluateTuiIdle( @@ -121,7 +126,7 @@ describe('evaluateTuiIdle ranking', () => { }) expect(evaluateTuiIdle(input({ ...noMuse, agent: 'claude', record: streaming }))).toEqual({ kind: 'pending', - quietForeground: false + quietForeground: 'closed' }) const quiet = record({ lastAgentStatus: 'idle', lastOscTitle: 'claude' }) expect(evaluateTuiIdle(input({ ...noMuse, agent: 'claude', record: quiet }))).toEqual({ @@ -145,18 +150,73 @@ describe('evaluateTuiIdle ranking', () => { firstPartyStatus: { state: 'blocked', updatedAt: Date.now() } }) ) - expect(verdict).toEqual({ kind: 'pending', quietForeground: false }) + expect(verdict).toEqual({ kind: 'pending', quietForeground: 'closed' }) }) - it('leaves the quiet-foreground lane open only for an unidentified pane with no title status', () => { + it('leaves the quiet-foreground lane open for an unidentified pane with no title status', () => { expect(evaluateTuiIdle(input({ ...noMuse, agent: null }))).toEqual({ kind: 'pending', - quietForeground: true + quietForeground: 'open' }) - expect(evaluateTuiIdle(input({ ...noMuse, agent: 'claude' }))).toEqual({ - kind: 'pending', - quietForeground: false + }) + + it('closes the quiet-foreground lane for an agent with a stronger rest signal still to come', () => { + for (const agent of ['claude', 'codex', 'grok', 'dsh'] as const) { + expect(evaluateTuiIdle(input({ ...noMuse, agent }))).toEqual({ + kind: 'pending', + quietForeground: 'closed' + }) + } + }) + + // Why: a launched agent whose title Orca cannot classify has no other lane; closing this + // one for every known agent left `worker start` failing at agent_readiness (STA-7440). + it('keeps the quiet-foreground lane for an agent with no other rest signal, after it paints', () => { + for (const agent of ['amp', 'goose', 'crush', 'kimi', 'qwen-code', 'rovo', 'aug'] as const) { + expect(evaluateTuiIdle(input({ ...noMuse, agent }))).toEqual({ + kind: 'pending', + quietForeground: 'after-paint' + }) + } + }) + + it('closes the lane once any title has classified, so a title that does arrive outranks it', () => { + const verdict = evaluateTuiIdle( + input({ ...noMuse, agent: 'amp', record: record({ lastAgentStatus: 'permission' }) }) + ) + expect(verdict).toEqual({ kind: 'pending', quietForeground: 'closed' }) + }) +}) + +// Why: `none` reopens the quiet-foreground lane, which is only safe where no stronger lane +// could have settled the wait; the identity-keyed lanes must agree with the declared signal. +describe('rest signal agrees with the lanes that can settle a wait', () => { + it.each(Object.keys(TUI_AGENT_CONFIG).filter(isTuiAgent))('%s', (agent) => { + const signal = getTuiAgentRestSignal(agent) + const hookDone = hasFreshDoneFirstPartyStatus(agent, { state: 'done', updatedAt: Date.now() }) + expect(hookDone).toBe(signal === 'hook-done') + let screenRead = false + isKnownReadyPromptBody('', agent, () => { + screenRead = true + return null }) + const quietScreenBody = hasQuietReadyScreen(record(), agent, () => true, QUIESCENCE_MS) + // Why not only ready-body: Codex keeps its stronger hook-driven title beside this lane. + if (quietScreenBody) { + expect(signal).not.toBe('none') + } + // Why a screen read also counts: Qoder's ready body is its composer, read by identity. + if (signal === 'ready-body') { + expect(quietScreenBody || screenRead).toBe(true) + } + if (signal !== 'none') { + return + } + expect({ + screenRead, + syntheticTitle: getSyntheticAgentTerminalTitle(agent, 'done'), + processTitle: detectAgentStatusFromTitle(TUI_AGENT_CONFIG[agent].expectedProcess) + }).toEqual({ screenRead: false, syntheticTitle: null, processTitle: null }) }) }) @@ -181,7 +241,7 @@ describe('a DSH pane settles tui-idle on its own hook', () => { record: { lastAgentStatus: null, lastOutputAt: null, lastOscTitle: '\u2726 \u{1F40B} repo' }, rendererTitle: undefined, readPositiveBodyEvidence: () => false, - readMuseReadyBodyEvidence: () => false, + readQuietReadyBodyEvidence: () => false, readTailBlockedReason: () => null, agent: 'dsh' as const, firstPartyStatus: { state: 'done' as const, updatedAt: Date.now() }, diff --git a/src/main/runtime/tui-idle-evidence.ts b/src/main/runtime/tui-idle-evidence.ts index 87027ab4bc3..cb7dc13714b 100644 --- a/src/main/runtime/tui-idle-evidence.ts +++ b/src/main/runtime/tui-idle-evidence.ts @@ -8,12 +8,13 @@ import type { RuntimeTerminalWaitBlockedReason } from '../../shared/runtime-type import { getSyntheticAgentTerminalTitle } from '../../shared/synthetic-agent-title' import { resolveExplicitTerminalTitleAgentType } from '../../shared/terminal-title-agent-type' import type { TuiAgent } from '../../shared/tui-agent' +import { getTuiAgentRestSignal } from '../../shared/tui-agent-rest-signal' import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' import { detectExplicitIdleStatusFromTitle, detectTerminalWaitBlockedReason, isKnownReadyPromptBody, - isMuseReadyPromptPreview + isQuietReadyScreenBody } from './terminal-wait-detection' /** @@ -28,8 +29,8 @@ import { * 0. BLOCKED — the tail shows a prompt waiting on the user. * 1. STRONG READY — the agent states it is ready: an explicit idle marker in its own * title, or a known ready-prompt body. - * 1b. MUSE — Muse emits no title signal at all, so its ready-screen body stands in - * for the strong evidence, believed only once the stream has gone quiet. + * 1b. QUIET READY SCREEN — Muse and an idle Codex title no rest signal, so their + * ready-screen body stands in for the strong evidence, believed only once quiet. * 2. WORKING — a fresh first-party agent status (OSC 9999) saying working/blocked/ * waiting, or a working title. The agent's own account of itself outranks anything * inferred. @@ -157,13 +158,22 @@ export function hasSustainedTitleIdle( } /** - * Tier 3, cold start: Orca launched a known agent on this PTY, so a quiet non-shell - * foreground process is an agent still booting, not one sitting at its prompt. Resolving - * on it is what let `dispatch --inject` write into a TUI that had not yet attached its - * reader and silently lose the prompt (#9976). + * When a quiet non-shell foreground process may settle a pending wait. + * - `closed`: the agent has a stronger rest signal, so its silence is boot, not rest. + * Resolving on it let `dispatch --inject` write into a TUI that had not yet attached + * its reader and silently lose the prompt (#9976). + * - `after-paint`: Orca knows an agent runs here but it has no other rest signal, so this + * lane is its only one. A TUI that has painted nothing yet is still booting. + * - `open`: nothing is known about the pane, so a missing output clock also counts as quiet + * (see isQuietForQuiescence). */ -export function quietForegroundProcessProvesTuiIdle(agent: TuiAgent | null | undefined): boolean { - return !agent +export type QuietForegroundLane = 'closed' | 'after-paint' | 'open' + +function quietForegroundLane(agent: TuiAgent | null | undefined): QuietForegroundLane { + if (!agent) { + return 'open' + } + return getTuiAgentRestSignal(agent) === 'none' ? 'after-paint' : 'closed' } export type TuiIdleEvaluationInput = { @@ -177,8 +187,8 @@ export type TuiIdleEvaluationInput = { * (~11us and a multi-KB string on a full tail); the title check below usually answers * first, and then none of that has to happen at all. */ readPositiveBodyEvidence: () => boolean - /** Tier 1b body evidence: a Muse ready screen. Thunk for the same reason as above. */ - readMuseReadyBodyEvidence: () => boolean + /** Tier 1b body evidence: a Muse or Codex ready screen. Thunk for the same reason as above. */ + readQuietReadyBodyEvidence: () => boolean agent: TuiAgent | null | undefined firstPartyStatus: FirstPartyAgentStatus quiescenceMs: number @@ -191,41 +201,40 @@ export type TuiIdleVerdict = | { kind: 'ready-weak' } /** The agent says it is mid-turn: nothing may settle, and its screen is not read. */ | { kind: 'working' } - /** `quietForeground`: whether a quiet non-shell foreground process may still settle it. */ - | { kind: 'pending'; quietForeground: boolean } + | { kind: 'pending'; quietForeground: QuietForegroundLane } const READY_STRONG: TuiIdleVerdict = { kind: 'ready-strong' } const READY_WEAK: TuiIdleVerdict = { kind: 'ready-weak' } const WORKING: TuiIdleVerdict = { kind: 'working' } +const QUIET_READY_SCREEN_AGENTS: ReadonlySet = new Set(['muse', 'codex']) + /** - * Tier 1b: a Muse ready screen in the body, believed only once the stream has gone quiet. + * Tier 1b: a ready screen in the body, believed only once the stream has gone quiet. * - * Muse is the one agent with no title signal at all — its OSC title is the bare cwd and - * never changes — so neither the explicit-idle nor the sustained-title lane can fire. - * The ready screen proves the TUI is up; the quiescence demand keeps a mid-turn - * streaming pane from satisfying, mirroring the codex tier-3 lane's - * positive-evidence-plus-quiet shape. Scoped to Muse and agent-unknown panes: another - * agent's scrollback quoting Muse must not settle its wait. + * Muse's OSC title is the bare cwd and never changes, and an idle Codex titles its pane with + * the cwd (plus a thread name) and no agent name, so neither the explicit-idle nor the + * sustained-title lane can fire. The ready screen proves the TUI is up; the quiescence + * demand keeps a mid-turn streaming pane from satisfying, mirroring the tier-3 lane's + * positive-evidence-plus-quiet shape. Scoped to those agents and agent-unknown panes (which + * read only Muse's screen): another agent's scrollback quoting them must not settle its wait. */ -export function hasQuietMuseReadyPrompt( +export function hasQuietReadyScreen( record: TuiIdleEvidenceRecord, agent: TuiAgent | null | undefined, readBodyEvidence: () => boolean, quiescenceMs: number ): boolean { - if (agent !== null && agent !== undefined && agent !== 'muse') { - return false - } - if (!readBodyEvidence()) { + if (agent && !QUIET_READY_SCREEN_AGENTS.has(agent)) { return false } // Why: same rule as the tier-3 lane — without an output clock there is no // corroboration available, so hold out instead of settling. - if (record.lastOutputAt === null) { + if (record.lastOutputAt === null || Date.now() - record.lastOutputAt < quiescenceMs) { return false } - return Date.now() - record.lastOutputAt >= quiescenceMs + // Why last: a streaming pane never pays for the screen projection. + return readBodyEvidence() } /** The one place the tiers are combined; every settle site branches only on the verdict. */ @@ -244,7 +253,7 @@ export function evaluateTuiIdle(input: TuiIdleEvaluationInput): TuiIdleVerdict { } return input.readPositiveBodyEvidence() ? READY_STRONG - : { kind: 'pending', quietForeground: false } + : { kind: 'pending', quietForeground: 'closed' } } // Why the title before the body: both are tier 1, so either settles, but the title is a // memoized lookup and the body is a fresh multi-KB scan. Same verdict, cheaper order. @@ -261,14 +270,16 @@ export function evaluateTuiIdle(input: TuiIdleEvaluationInput): TuiIdleVerdict { // when a dialog is on screen, so the screen read must still run. return input.firstPartyStatus?.state === 'working' ? WORKING - : { kind: 'pending', quietForeground: false } + : { kind: 'pending', quietForeground: 'closed' } } // Why after the veto: a first-party working account outranks inferred body evidence. + // Why before the working title: Codex can leave a stale spinner title after a turn, and a + // live spinner emits output every ~100 ms, so a spinning pane is never quiet here. if ( - hasQuietMuseReadyPrompt( + hasQuietReadyScreen( input.record, input.agent, - input.readMuseReadyBodyEvidence, + input.readQuietReadyBodyEvidence, input.quiescenceMs ) ) { @@ -283,7 +294,7 @@ export function evaluateTuiIdle(input: TuiIdleEvaluationInput): TuiIdleVerdict { return { kind: 'pending', quietForeground: - input.record.lastAgentStatus === null && quietForegroundProcessProvesTuiIdle(input.agent) + input.record.lastAgentStatus === null ? quietForegroundLane(input.agent) : 'closed' } } @@ -319,7 +330,8 @@ export function leafTuiIdleEvidence( rendererTitle: leaf.paneTitle ?? source.getTabTitle(leaf.tabId), readPositiveBodyEvidence: () => isKnownReadyPromptBody(waitText(), agent, () => source.readScreenLines(leaf.ptyId)), - readMuseReadyBodyEvidence: () => isMuseReadyPromptPreview(waitText()), + readQuietReadyBodyEvidence: () => + isQuietReadyScreenBody(waitText(), agent, () => source.readScreenLines(leaf.ptyId)), agent, firstPartyStatus: source.getFirstPartyAgentStatus(leaf.ptyId), quiescenceMs: source.quiescenceMs @@ -339,7 +351,8 @@ export function ptyTuiIdleEvidence( readPositiveBodyEvidence: () => (agent !== 'qoder' && source.getAdoptedPtyIdleStatus(pty) === 'idle') || isKnownReadyPromptBody(waitText(), agent, () => source.readScreenLines(pty.ptyId)), - readMuseReadyBodyEvidence: () => isMuseReadyPromptPreview(waitText()), + readQuietReadyBodyEvidence: () => + isQuietReadyScreenBody(waitText(), agent, () => source.readScreenLines(pty.ptyId)), agent, firstPartyStatus: source.getFirstPartyAgentStatus(pty.ptyId), quiescenceMs: source.quiescenceMs diff --git a/src/relay/opencode-canonical-config.ts b/src/relay/opencode-canonical-config.ts index 4bcb9f3e151..38682d3f274 100644 --- a/src/relay/opencode-canonical-config.ts +++ b/src/relay/opencode-canonical-config.ts @@ -1,6 +1,7 @@ import { mkdirSync, unlinkSync, writeFileSync } from 'node:fs' import { isAbsolute, join, relative, resolve } from 'node:path' import { resolveOpenCodeConfigDirectory } from '../shared/opencode-config-directory' +import { isInstalledOpenCodePluginCurrent } from '../shared/opencode-installed-plugin' const RELAY_HOOKS_DIR = '.orca-relay' @@ -18,12 +19,14 @@ export function installOpenCodePluginInCanonicalConfig( agent === 'opencode2' ? 'orca-opencode2-status.js' : 'orca-opencode-status.js' const pluginPath = join(configDir, 'plugins', pluginFileName) mkdirSync(join(configDir, 'plugins'), { recursive: true }) - try { - unlinkSync(pluginPath) - } catch { - // The file may not exist on the first install. + if (!isInstalledOpenCodePluginCurrent(pluginPath, source)) { + try { + unlinkSync(pluginPath) + } catch { + // The file may not exist on the first install. + } + writeFileSync(pluginPath, source) } - writeFileSync(pluginPath, source) return true } catch (err) { process.stderr.write( diff --git a/src/relay/plugin-overlay.test.ts b/src/relay/plugin-overlay.test.ts index 14feb631876..d4d38a31fba 100644 --- a/src/relay/plugin-overlay.test.ts +++ b/src/relay/plugin-overlay.test.ts @@ -1,12 +1,15 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { existsSync, + lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, + statSync, symlinkSync, + utimesSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -74,6 +77,50 @@ describe('PluginOverlayManager', () => { ).toBe('v2 plugin') }) + // Why: OpenCode 2 reloads a plugin whose file mtime changed, even with unchanged bytes. + it('leaves a current canonical plugin untouched and replaces a stale one', () => { + const env = { XDG_CONFIG_HOME: join(homeDir, 'xdg') } + const pluginPath = join(homeDir, 'xdg', 'opencode', 'plugins', 'orca-opencode2-status.js') + manager.setSources({ opencode2PluginSource: 'v2 plugin' }) + manager.installOpenCodePlugin('opencode2', env) + const past = new Date('2020-01-01T00:00:00Z') + utimesSync(pluginPath, past, past) + + expect(manager.installOpenCodePlugin('opencode2', env)).toBe(true) + expect(statSync(pluginPath).mtimeMs).toBe(past.getTime()) + + manager.setSources({ opencode2PluginSource: 'v2 plugin, next release' }) + expect(manager.installOpenCodePlugin('opencode2', env)).toBe(true) + expect(readFileSync(pluginPath, 'utf8')).toBe('v2 plugin, next release') + }) + + // Why: OpenCode 2 loads through a file-level symlink (dotfile managers) and stats its target. + it.skipIf(process.platform === 'win32')( + 'leaves a symlinked canonical plugin with current bytes untouched', + () => { + const env = { XDG_CONFIG_HOME: join(homeDir, 'xdg') } + const pluginsDir = join(homeDir, 'xdg', 'opencode', 'plugins') + const pluginPath = join(pluginsDir, 'orca-opencode2-status.js') + const targetPath = join(homeDir, 'dotfiles-orca-opencode2-status.js') + mkdirSync(pluginsDir, { recursive: true }) + writeFileSync(targetPath, 'v2 plugin') + symlinkSync(targetPath, pluginPath) + const past = new Date('2020-01-01T00:00:00Z') + utimesSync(targetPath, past, past) + manager.setSources({ opencode2PluginSource: 'v2 plugin' }) + + expect(manager.installOpenCodePlugin('opencode2', env)).toBe(true) + expect(lstatSync(pluginPath).isSymbolicLink()).toBe(true) + expect(statSync(targetPath).mtimeMs).toBe(past.getTime()) + + manager.setSources({ opencode2PluginSource: 'v2 plugin, next release' }) + expect(manager.installOpenCodePlugin('opencode2', env)).toBe(true) + expect(lstatSync(pluginPath).isFile()).toBe(true) + expect(readFileSync(pluginPath, 'utf8')).toBe('v2 plugin, next release') + expect(readFileSync(targetPath, 'utf8')).toBe('v2 plugin') + } + ) + it('mirrors a preexisting remote OpenCode config dir before adding Orca plugin', () => { const userConfigDir = join(homeDir, 'company-opencode') mkdirSync(join(userConfigDir, 'plugins'), { recursive: true }) diff --git a/src/renderer/src/components/editor/DiffSectionBody.tsx b/src/renderer/src/components/editor/DiffSectionBody.tsx index 869d0de5a73..5ac46e17538 100644 --- a/src/renderer/src/components/editor/DiffSectionBody.tsx +++ b/src/renderer/src/components/editor/DiffSectionBody.tsx @@ -166,6 +166,8 @@ export function DiffSectionBody({ keepCurrentOriginalModel keepCurrentModifiedModel options={{ + // Native EditContext focus scrolls its hidden input into the combined viewport. + editContext: false, readOnly: !isEditable, originalEditable: false, renderSideBySide: sideBySide, diff --git a/src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.test.ts b/src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.test.ts new file mode 100644 index 00000000000..7f58e7c2a37 --- /dev/null +++ b/src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest' +import { getCombinedDiffRenderRange } from './combined-diff-render-range' + +describe('combined diff render range', () => { + it('keeps visible large sections without mounting their large neighbors', () => { + expect( + getCombinedDiffRenderRange( + { startIndex: 2, endIndex: 3, count: 8, overscan: 5 }, + () => 230_000, + 800 + ) + ).toEqual([2, 3]) + }) + + it('pre-renders at most one viewport of small sections on each side', () => { + expect( + getCombinedDiffRenderRange( + { startIndex: 4, endIndex: 5, count: 10, overscan: 5 }, + () => 200, + 400 + ) + ).toEqual([2, 3, 4, 5, 6, 7]) + }) + + it('stops at a large neighboring section instead of skipping across it', () => { + const heights = [100, 230_000, 100, 100, 230_000, 100] + expect( + getCombinedDiffRenderRange( + { startIndex: 2, endIndex: 2, count: heights.length, overscan: 5 }, + (index) => heights[index], + 800 + ) + ).toEqual([2, 3]) + }) + + it('retains the row-count cap and list boundaries for short sections', () => { + expect( + getCombinedDiffRenderRange( + { startIndex: 1, endIndex: 2, count: 100, overscan: 5 }, + () => 28, + 800 + ) + ).toEqual([0, 1, 2, 3, 4, 5, 6, 7]) + }) + + it('renders only visible sections before the viewport is measured', () => { + expect( + getCombinedDiffRenderRange( + { startIndex: 1, endIndex: 2, count: 10, overscan: 5 }, + () => 88, + 0 + ) + ).toEqual([1, 2]) + }) +}) diff --git a/src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.ts b/src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.ts new file mode 100644 index 00000000000..0e57d785e86 --- /dev/null +++ b/src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.ts @@ -0,0 +1,28 @@ +import type { Range } from '@tanstack/react-virtual' + +export function getCombinedDiffRenderRange( + range: Range, + estimateHeight: (index: number) => number, + viewportHeight: number +): number[] { + let start = range.startIndex + let end = range.endIndex + const budget = Math.max(0, viewportHeight) + let height = 0 + while (start > 0 && range.startIndex - start < range.overscan) { + height += estimateHeight(start - 1) + if (height > budget) { + break + } + start -= 1 + } + height = 0 + while (end + 1 < range.count && end - range.endIndex < range.overscan) { + height += estimateHeight(end + 1) + if (height > budget) { + break + } + end += 1 + } + return Array.from({ length: end - start + 1 }, (_, offset) => start + offset) +} diff --git a/src/renderer/src/components/editor/combined-diff/scroll-viewport/use-combined-diff-virtualizer.ts b/src/renderer/src/components/editor/combined-diff/scroll-viewport/use-combined-diff-virtualizer.ts index d9874ea9265..4cca1137aff 100644 --- a/src/renderer/src/components/editor/combined-diff/scroll-viewport/use-combined-diff-virtualizer.ts +++ b/src/renderer/src/components/editor/combined-diff/scroll-viewport/use-combined-diff-virtualizer.ts @@ -4,6 +4,7 @@ import { elementScroll, useVirtualizer, type Virtualizer } from '@tanstack/react import type { ProgrammaticScrollMarks } from '@/hooks/programmatic-scroll-marks' import type { DiffSection } from '../../diff-section-types' import { getDiffSectionRowEstimatedHeight } from '../../diff-section-layout' +import { getCombinedDiffRenderRange } from './combined-diff-render-range' const COMBINED_DIFF_OVERSCAN = 5 @@ -28,17 +29,21 @@ export function useCombinedDiffVirtualizer({ sections: DiffSection[] sideBySide: boolean }): Virtualizer { + const estimateSize = (index: number): number => { + const section = sections[index] + return section ? getDiffSectionRowEstimatedHeight(section, sectionHeights[index]) : 88 + } const virtualizer = useVirtualizer({ count: sections.length, getScrollElement: () => scrollContainerRef.current, - estimateSize: (index) => { - const section = sections[index] - if (!section) { - return 88 - } - - return getDiffSectionRowEstimatedHeight(section, sectionHeights[index]) - }, + estimateSize, + // Offscreen Monaco sections render their full height; bound that work by pixels, too. + rangeExtractor: (range) => + getCombinedDiffRenderRange( + range, + estimateSize, + scrollContainerRef.current?.clientHeight ?? 0 + ), overscan: COMBINED_DIFF_OVERSCAN, initialOffset: () => scrollOffsetRef.current, // Why: mark every virtualizer-issued scroll so events are attributed to the user only when this code didn't cause them. diff --git a/src/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.tsx b/src/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.tsx index 2e8cf51f18f..de9ba8c655c 100644 --- a/src/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.tsx +++ b/src/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.tsx @@ -178,10 +178,10 @@ export function NativeChatBackgroundTasksStatus(props: { className="group/tasks shrink-0 bg-background px-3 pt-2 sm:px-4" > {/* When the goal tab is the next sibling, take its width and share its top edge. */} -

+
diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx index 3562065c151..d739e0b8d61 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx @@ -42,7 +42,11 @@ import { nativeChatReaderScrollInputHandlers } from './native-chat-reader-scroll import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types' import { isStructuredAgentSessionThinking } from '../../../../shared/structured-agent-session-live-turn' -import type { NativeChatSettledTurns } from '../../../../shared/native-chat-turn-status' +import { nativeChatSubagentLabels } from '../../../../shared/native-chat-subagent-attribution' +import { + selectNativeChatActiveTurnKey, + type NativeChatSettledTurns +} from '../../../../shared/native-chat-turn-status' import { nativeChatTurnDiffs, type NativeChatDiffReveal, @@ -70,6 +74,7 @@ export function NativeChatMessageList({ allowFileUriLinks = false, workingStartedAt, settledTurns, + activeTurnOpenedBy, deliveryNotices, showTurnStatus = true, showLiveTurnActivity = true, @@ -89,6 +94,8 @@ export function NativeChatMessageList({ workingStartedAt?: number | null /** Host-recorded turn durations keyed by user message id (structured lane). */ settledTurns?: NativeChatSettledTurns + /** The user message the host says opened the running turn (structured lane). */ + activeTurnOpenedBy?: string | null onLinkClick?: CommentMarkdownLinkClickHandler allowFileUriLinks?: boolean deliveryNotices?: ReadonlyMap @@ -147,14 +154,13 @@ export function NativeChatMessageList({ // Structured sessions show goal state in the banner above the composer. return journalItems ? omitNativeChatThreadGoalRows(projected) : projected }, [journalItems, projectMessages, session.messages]) + const subagentLabels = useMemo(() => nativeChatSubagentLabels(messages), [messages]) const taskListPredecessors = useMemo(() => nativeChatTaskListPredecessors(messages), [messages]) const taskListState = useMemo(() => nativeChatTaskListState(messages), [messages]) const showTypingIndicator = showTurnStatus ? isWorking : shouldShowNativeChatTypingIndicator({ messages, isWorking }) - const latestUserIndex = messages.findLastIndex((message) => message.role === 'user') - const currentTurnKey = - latestUserIndex === -1 ? undefined : (messages[latestUserIndex]?.id ?? undefined) + const currentTurnKey = messages.findLast((message) => message.role === 'user')?.id ?? undefined // Resolve each row's turn boundary once. Prefix slice/findLast in the render // loop becomes quadratic for long transcripts. const turnKeys = useMemo(() => { @@ -179,9 +185,10 @@ export function NativeChatMessageList({ () => (journalItems ? isStructuredAgentSessionThinking(journalItems) : false), [journalItems] ) + const activeTurnKey = selectNativeChatActiveTurnKey(messages, activeTurnOpenedBy) const turnStatuses = useNativeChatTurnStatus({ messages, - latestUserIndex, + activeTurnKey, isWorking: showTurnStatus && isWorking, workingStartedAt: showTurnStatus ? workingStartedAt : null, settledTurns: showTurnStatus ? settledTurns : null, @@ -193,7 +200,7 @@ export function NativeChatMessageList({ buildNativeChatTranscriptSlots({ messages, turnKeys, - latestUserIndex, + activeTurnKey, currentTurnKey, receipts, turnStatuses, @@ -201,17 +208,19 @@ export function NativeChatMessageList({ showTurnStatus, expandedTurnKeys: expandedTurnIds, isWorking, - lifecycleWorking + lifecycleWorking, + subagentLabels }), [ + activeTurnKey, currentTurnKey, expandedTurnIds, isWorking, - latestUserIndex, lifecycleWorking, messages, receipts, showTurnStatus, + subagentLabels, turnDiffs, turnKeys, turnStatuses diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx index b58c91fe31f..8400b592e7d 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx @@ -655,6 +655,93 @@ describe('NativeChatMessageList turn indicator', () => { expect(screen.getByText('Working for 0s')).toBeInTheDocument() }) + it('keeps the live bar under the prompt that opened the running turn, not a message sent mid-turn', () => { + const prompt = (id: string, text: string): NativeChatLiveSession['messages'][number] => ({ + id, + role: 'user', + blocks: [{ type: 'text', text }], + timestamp: 1, + source: 'transcript' + }) + const messages = [ + prompt('user-a', 'Run four sleeps'), + { + id: 'tool-a', + role: 'assistant' as const, + blocks: [ + { + type: 'tool-call' as const, + name: 'Bash', + input: { command: 'sleep 15' }, + state: 'running' as const + } + ], + timestamp: 2, + source: 'transcript' as const + }, + prompt('user-b', 'Also say banana') + ] + const list = (props: Partial>) => ( + + ) + // B was sent while A's turn runs: the host still names A as the running turn's opener. + const { rerender } = render( + list({ + workingStartedAt: Date.now() - 5000, + settledTurns: new Map(), + activeTurnOpenedBy: 'user-a' + }) + ) + const barA = screen.getByText('Working for 5s').closest('[data-native-chat-turn-status]') + expect(screen.getAllByText(/Working for/)).toHaveLength(1) + expect(screen.getByText('Run four sleeps').compareDocumentPosition(barA!)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ) + expect(barA!.compareDocumentPosition(screen.getByText('Also say banana'))).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ) + + // B's own turn opens: A settles in place and B counts from A's end. + rerender( + list({ + workingStartedAt: Date.now() - 1000, + settledTurns: new Map([['user-a', { startedAt: 1, workedSeconds: 17 }]]), + activeTurnOpenedBy: 'user-b' + }) + ) + expect(screen.getByText('Worked for 17s').closest('[data-native-chat-turn-status]')).toBe(barA) + const barB = screen.getByText('Working for 1s').closest('[data-native-chat-turn-status]') + expect(screen.getByText('Also say banana').compareDocumentPosition(barB!)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ) + + rerender( + list({ + isWorking: false, + session: { ...session, status: 'ready', messages }, + workingStartedAt: null, + settledTurns: new Map([ + ['user-a', { startedAt: 1, workedSeconds: 17 }], + ['user-b', { startedAt: 2, workedSeconds: 51 }] + ]), + activeTurnOpenedBy: null + }) + ) + expect(screen.getByText('Worked for 17s').closest('[data-native-chat-turn-status]')).toBe(barA) + expect( + screen + .getByText('Also say banana') + .compareDocumentPosition(screen.getByText('Worked for 51s')) + ).toBe(Node.DOCUMENT_POSITION_FOLLOWING) + expect(screen.queryByText(/Working for/)).toBeNull() + }) + it("uses the completed caret to expand that turn's tool details", () => { const startedAt = Date.now() - 3000 render( diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx index f11e41a6e2d..7097cb87aa4 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx @@ -60,9 +60,9 @@ describe('MessageRow control visibility', () => { 'can-hover:opacity-0', 'can-hover:pointer-events-none', 'group-hover:opacity-100', - 'group-has-[:focus-visible]:opacity-100', + '[.group:has(:focus-visible)_&]:opacity-100', 'group-hover:pointer-events-auto', - 'group-has-[:focus-visible]:pointer-events-auto' + '[.group:has(:focus-visible)_&]:pointer-events-auto' ) expect(copy.parentElement).not.toHaveClass('opacity-0', 'pointer-events-none') expect(time).not.toHaveAttribute('tabindex') @@ -79,9 +79,9 @@ describe('MessageRow control visibility', () => { 'can-hover:opacity-0', 'can-hover:pointer-events-none', 'group-hover:opacity-100', - 'group-has-[:focus-visible]:opacity-100', + '[.group:has(:focus-visible)_&]:opacity-100', 'group-hover:pointer-events-auto', - 'group-has-[:focus-visible]:pointer-events-auto' + '[.group:has(:focus-visible)_&]:pointer-events-auto' ) expect(copy.parentElement).not.toHaveClass('opacity-0', 'pointer-events-none') expect(copy.parentElement!.parentElement).toHaveClass('group') @@ -204,3 +204,41 @@ describe('a user message that did not go through', () => { expect(screen.queryByRole('button', { name: 'Retry' })).toBeNull() }) }) + +describe("MessageRow — a subagent's row speaks as that subagent", () => { + function renderAgentRow(agentId: string | undefined, subagentLabel?: string) { + return render( + + ) + } + + it('names the subagent that wrote the row', () => { + renderAgentRow('task-1', 'explore the lane') + expect( + screen.getByRole('note', { name: 'Written by subagent explore the lane' }) + ).toBeInTheDocument() + expect(screen.getByText('The PR is CLEAN.')).toBeInTheDocument() + }) + + it('still marks the row as a subagent when no loaded roster names it', () => { + renderAgentRow('task-9') + expect(screen.getByRole('note', { name: 'Subagent' })).toBeInTheDocument() + }) + + it("adds nothing to the session's own row", () => { + renderAgentRow(undefined, 'explore the lane') + expect(screen.queryByRole('note')).toBeNull() + }) +}) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx index c7ed087e918..59e70cfc7e1 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx @@ -1,5 +1,5 @@ import { memo, useCallback, useRef } from 'react' -import { Goal, RotateCcw } from 'lucide-react' +import { Bot, Goal, RotateCcw } from 'lucide-react' import { Button } from '@/components/ui/button' import CommentMarkdown, { type CommentMarkdownLinkClickHandler @@ -11,6 +11,8 @@ import type { NativeChatToolCallBlock } from '../../../../shared/native-chat-types' import { deriveNativeChatRowContent } from '../../../../shared/native-chat-row-content' +import { agentJournalItemSubagentId } from '../../../../shared/agent-session-journal-producer' +import { NATIVE_CHAT_SUBAGENT_ATTRIBUTION_COPY } from '../../../../shared/native-chat-subagent-attribution' import { NativeChatToolRun } from './NativeChatToolRun' import { NativeChatCodeBlock } from './NativeChatCodeBlock' import { NativeChatNoticeRow } from './NativeChatNoticeRow' @@ -47,6 +49,7 @@ export const MessageRow = memo(function MessageRow({ deliveryNotice, structuredActivityUi = true, folded = false, + subagentLabel, runtimeContext }: { message: NativeChatMessage @@ -65,6 +68,8 @@ export const MessageRow = memo(function MessageRow({ structuredActivityUi?: boolean /** Behind a folded turn: the row keeps only what outlives the turn. */ folded?: boolean + /** The roster's name for the subagent that wrote this row, when one names it. */ + subagentLabel?: string runtimeContext?: RuntimeFileOperationArgs | null }): React.JSX.Element | null { const rowRef = useRef(null) @@ -167,7 +172,7 @@ export const MessageRow = memo(function MessageRow({ {/* Copy + timestamp reveal together, mirroring the agent controls row. Image-only prompts have no text to copy, so the button is omitted. */} {markdown || message.timestamp !== null ? ( -
+
{markdown ? : null}
@@ -193,6 +198,24 @@ export const MessageRow = memo(function MessageRow({ // Plain assistant prose is the copyable unit; reasoning/system asides stay // chrome-free. Controls reveal on hover/keyboard focus and stay visible on touch. const showControls = !isReasoning && !isSystem && markdown.length > 0 + // A subagent's row sits where it happened but speaks as that subagent, never as + // the agent the reader is talking to. + const subagentName = + agentJournalItemSubagentId(message) === null + ? null + : (subagentLabel ?? + translate( + 'components.native-chat.subagents.unnamed', + NATIVE_CHAT_SUBAGENT_ATTRIBUTION_COPY.unnamed + )) + const subagentCaption = + subagentLabel === undefined + ? subagentName + : translate( + 'components.native-chat.subagents.writtenBy', + NATIVE_CHAT_SUBAGENT_ATTRIBUTION_COPY.writtenBy, + { value0: subagentLabel } + ) return (
+ {subagentName !== null ? ( +
+ + + {subagentName} + +
+ ) : null} ) : null}
diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx index 471f1b636d2..c3c08f6647e 100644 --- a/src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx @@ -35,7 +35,7 @@ const STRIP = '[data-native-chat-background-tasks]' const GOAL = '[data-native-chat-thread-goal]' // The seam is CSS on DOM adjacency: the strip styles itself when a goal tab follows // it, and the goal tab styles itself when the strip precedes it. -const STRIP_DOCK_RULE = /^group-has-\[\+\[([a-z-]+)\]\]\/tasks:(.+)$/ +const STRIP_DOCK_RULE = /^\[\[data-native-chat-background-tasks\]:has\(\+\[([a-z-]+)\]\)_&\]:(.+)$/ const GOAL_DOCK_RULE = /^group-\[\[([a-z-]+)\]\+&\]\/goal:(.+)$/ function dockRules(root: Element, rule: RegExp): { attribute: string; utility: string }[] { diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx index 6daa3d3f62c..2de5591b779 100644 --- a/src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx +++ b/src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx @@ -22,7 +22,7 @@ import { NativeChatStructuredSessionStatus } from './NativeChatStructuredSession import { useNativeChatLaunchDraftSignal } from './use-native-chat-launch-draft-adoption' import { NativeChatLaunchRetry } from './NativeChatLaunchRetry' import { useNativeChatProvisionalLaunch } from './use-native-chat-provisional-launch' -import { useStructuredAgentSessionHostExecutionPhase } from './StructuredAgentSessionStatusBridge' +import { useStructuredAgentSessionHostExecution } from './StructuredAgentSessionStatusBridge' import { structuredAgentLabel } from '@/lib/structured-agent-session-launch-label' import { NativeChatThreadGoalBanner } from './NativeChatThreadGoalBanner' import { structuredAgentSessionReadFailureNotice } from './structured-agent-session-read-failure-notice' @@ -42,10 +42,10 @@ export function NativeChatStructuredSession( ) const { sendThroughRelaunch } = provisionalLaunch // The host's own word on whether the provider child has answered startup yet. - const startupPhase = useStructuredAgentSessionHostExecutionPhase(props.sessionId, props.target) + const hostExecution = useStructuredAgentSessionHostExecution(props.sessionId, props.target) const controller = useStructuredAgentSession({ ...props, - providerStarting: startupPhase === 'starting', + providerStarting: hostExecution.phase === 'starting', transportEnabled: provisionalLaunch.transportEnabled, ...(provisionalLaunch.launch ? { launch: provisionalLaunch.launch } : {}) }) @@ -279,6 +279,7 @@ export function NativeChatStructuredSession( fontScale={fontScale.scale} workingStartedAt={controller.workingStartedAt} settledTurns={controller.settledTurns} + activeTurnOpenedBy={controller.activeTurnOpenedBy} showTurnStatus showLiveTurnActivity={prompt === null} turnActivity={controller.turnActivity} @@ -298,7 +299,8 @@ export function NativeChatStructuredSession( { + cleanup() + vi.useRealTimers() +}) const NO_TASKS = { show: false, @@ -17,12 +24,17 @@ const NO_TASKS = { supportsStopAll: false } -function renderStatus(startupPhase: 'starting' | 'ready' | null) { - return render( +function statusElement( + startupPhase: 'starting' | 'ready' | null, + sessionId = 'session-1', + startupChildKey: string | null = null +) { + return ( { - it('says the agent is still starting while the host reports a starting child', () => { + it('stays quiet through a normal startup', () => { + vi.useFakeTimers() + const view = renderStatus('starting') + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS - 1)) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + view.rerender(statusElement('ready')) + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS)) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + }) + + it('says the agent is still starting once startup runs long', () => { + vi.useFakeTimers() renderStatus('starting') + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS)) expect(screen.getByText(/Claude is still starting/)).toBeInTheDocument() expect(screen.getByText(/close this chat/)).toBeInTheDocument() }) + it('restarts the grace period for a relaunch or another session', () => { + vi.useFakeTimers() + const view = renderStatus('starting') + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS)) + view.rerender(statusElement('ready')) + act(() => vi.advanceTimersByTime(STATUS_MIN_VISIBLE_MS)) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + view.rerender(statusElement('starting')) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS)) + expect(screen.getByText(/still starting/)).toBeInTheDocument() + view.rerender(statusElement('starting', 'session-2')) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + }) + + it('hides an old notice and gives a replacement child its full grace period', () => { + vi.useFakeTimers() + const view = render(statusElement('starting', 'session-1', 'child-1')) + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS)) + expect(screen.getByText(/still starting/)).toBeInTheDocument() + view.rerender(statusElement('ready', 'session-1', 'child-1')) + act(() => vi.advanceTimersByTime(100)) + view.rerender(statusElement('starting', 'session-1', 'child-2')) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + act(() => vi.advanceTimersByTime(STATUS_MIN_VISIBLE_MS)) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS - STATUS_MIN_VISIBLE_MS)) + expect(screen.getByText(/still starting/)).toBeInTheDocument() + }) + + it('resets for a replacement child even when no intermediate phase reaches the view', () => { + vi.useFakeTimers() + const view = render(statusElement('starting', 'session-1', 'child-1')) + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS)) + view.rerender(statusElement('starting', 'session-1', 'child-2')) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS - 1)) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + act(() => vi.advanceTimersByTime(1)) + expect(screen.getByText(/still starting/)).toBeInTheDocument() + }) + + it('resets on an observed restart when an older host has no child identity', () => { + vi.useFakeTimers() + const view = renderStatus('starting') + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS)) + view.rerender(statusElement('ready')) + act(() => vi.advanceTimersByTime(100)) + view.rerender(statusElement('starting')) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + act(() => vi.advanceTimersByTime(SLOW_STARTUP_NOTICE_DELAY_MS - 1)) + expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() + act(() => vi.advanceTimersByTime(1)) + expect(screen.getByText(/still starting/)).toBeInTheDocument() + }) + it('shows nothing about startup once the child is ready or the host has no word', () => { renderStatus('ready') expect(screen.queryByText(/still starting/)).not.toBeInTheDocument() diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsx index 2f33b0ed0c3..31518886150 100644 --- a/src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsx +++ b/src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { translate } from '@/i18n/i18n' +import { useDelayedStatus } from '@/hooks/use-delayed-status' import { NativeChatBackgroundTasksStatus } from './NativeChatBackgroundTasksStatus' import type { StructuredSessionBackgroundTasksView } from './structured-session-background-tasks-view' @@ -11,12 +12,16 @@ type StoppingBackgroundTasks = { const NO_STOPPING_TASKS: ReadonlySet = new Set() +// Why: every launch passes through `starting`; only a slow start deserves the notice. +export const SLOW_STARTUP_NOTICE_DELAY_MS = 5_000 + export function NativeChatStructuredSessionStatus(props: { sessionId: string /** What to call the agent in copy about its process. */ agentLabel: string /** The host's word on the provider child; `starting` is published but not yet answering. */ startupPhase: 'starting' | 'ready' | null + startupChildKey: string | number | null error: string | null /** A read the pane is reconnecting on its own: said plainly, not as an error. */ reconnecting?: boolean @@ -28,6 +33,11 @@ export function NativeChatStructuredSessionStatus(props: { const [stopping, setStopping] = useState(null) const [expanded, setExpanded] = useState<{ sessionId: string; expanded: boolean } | null>(null) const activeStopping = stopping?.sessionId === props.sessionId ? stopping : null + const shownStartupPhase = useDelayedStatus( + JSON.stringify([props.sessionId, typeof props.startupChildKey, props.startupChildKey]), + props.startupPhase === 'starting' ? 'starting' : null, + SLOW_STARTUP_NOTICE_DELAY_MS + ) const onStop = (taskId?: string) => { const sessionId = props.sessionId @@ -61,7 +71,7 @@ export function NativeChatStructuredSessionStatus(props: { return ( <> - {props.startupPhase === 'starting' ? ( + {shownStartupPhase === 'starting' ? (

{translate( 'auto.components.native.chat.NativeChatStructuredSessionStatus.starting', diff --git a/src/renderer/src/components/native-chat/NativeChatTranscriptRow.tsx b/src/renderer/src/components/native-chat/NativeChatTranscriptRow.tsx index fb58acb7280..e5313d24d87 100644 --- a/src/renderer/src/components/native-chat/NativeChatTranscriptRow.tsx +++ b/src/renderer/src/components/native-chat/NativeChatTranscriptRow.tsx @@ -64,6 +64,7 @@ export const NativeChatTranscriptRow = memo(function NativeChatTranscriptRow({ deliveryNotice={context.deliveryNotices?.get(message.id)} structuredActivityUi={context.showTurnStatus} folded={slot.folded} + subagentLabel={slot.subagentLabel} runtimeContext={context.runtimeContext} /> )} diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx index 6cdaa2646e4..42091c79de9 100644 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx +++ b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx @@ -64,7 +64,7 @@ vi.mock('@/runtime/structured-agent-session-client', () => ({ import { getStructuredAgentSessionTabs, StructuredAgentSessionStatusBridge, - useStructuredAgentSessionHostExecutionPhase + useStructuredAgentSessionHostExecution } from './StructuredAgentSessionStatusBridge' import { resetStructuredAgentSessionStatusFeedsForTests } from '@/runtime/structured-agent-session-status-feed' @@ -698,28 +698,102 @@ describe('StructuredAgentSessionStatusBridge', () => { expect(mocks.setAgentStatus).not.toHaveBeenCalled() }) - it('re-renders a startup-phase reader only when the phase changes', async () => { - const phases: (string | null)[] = [] + it('re-renders a startup reader only when its phase or child changes', async () => { + const executions: ReturnType[] = [] function PhaseProbe(): null { - phases.push(useStructuredAgentSessionHostExecutionPhase('session-1', { kind: 'local' })) + executions.push(useStructuredAgentSessionHostExecution('session-1', { kind: 'local' })) return null } render() await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledOnce()) - act(() => feed().emit({ type: 'status', session: summary({ hostExecutionPhase: 'starting' }) })) - const rendersWhileStarting = phases.length act(() => feed().emit({ type: 'status', - session: summary({ hostExecutionPhase: 'starting', latestPrompt: 'next', updatedAt: 2 }) + session: summary({ + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: 'child-1', fence: 1 } + }) }) ) - expect(phases).toHaveLength(rendersWhileStarting) + const rendersWhileStarting = executions.length + act(() => + feed().emit({ + type: 'status', + session: summary({ + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: 'child-1', fence: 1 }, + latestPrompt: 'next', + updatedAt: 2 + }) + }) + ) + expect(executions).toHaveLength(rendersWhileStarting) + act(() => + feed().emit({ + type: 'status', + session: summary({ + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: 'child-1', fence: 2 } + }) + }) + ) + expect(executions).toHaveLength(rendersWhileStarting) - act(() => feed().emit({ type: 'status', session: summary({ hostExecutionPhase: 'ready' }) })) - expect(phases.at(-1)).toBe('ready') - expect(phases).toContain('starting') + act(() => + feed().emit({ + type: 'status', + session: summary({ + hostExecutionPhase: 'ready', + hostExecutionChild: { generation: 'child-1', fence: 1 } + }) + }) + ) + expect(executions.at(-1)?.phase).toBe('ready') + act(() => + feed().emit({ + type: 'status', + session: summary({ + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: 'child-2', fence: 2 } + }) + }) + ) + expect(executions.at(-1)).toEqual({ + phase: 'starting', + childKey: 'child-2' + }) + expect(executions.some(({ phase }) => phase === 'starting')).toBe(true) + }) + + it('uses the fence for a child whose acquisition has no generation', async () => { + const executions: ReturnType[] = [] + function PhaseProbe(): null { + executions.push(useStructuredAgentSessionHostExecution('session-1', { kind: 'local' })) + return null + } + render() + await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledOnce()) + act(() => + feed().emit({ + type: 'status', + session: summary({ + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: null, fence: 1 } + }) + }) + ) + expect(executions.at(-1)?.childKey).toBe(1) + act(() => + feed().emit({ + type: 'status', + session: summary({ + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: null, fence: 2 } + }) + }) + ) + expect(executions.at(-1)?.childKey).toBe(2) }) }) diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx index b0c546b3f9b..8205d96a5af 100644 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx +++ b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx @@ -47,18 +47,30 @@ export function useStructuredAgentSessionStatusSummary( return { summary, observation } } -/** Only the host's startup phase, so a chat re-renders when that changes, not on every status. */ -export function useStructuredAgentSessionHostExecutionPhase( +/** The host's child state, projected to stable primitives so journal updates do not re-render chat. */ +export function useStructuredAgentSessionHostExecution( sessionId: string, target: RuntimeClientTarget -): NonNullable | null { +): { + phase: NonNullable | null + childKey: string | number | null +} { const feed = useMemo(() => getStructuredAgentSessionStatusFeed(target), [target]) useEffect(() => feed.activate(), [feed]) - return useSyncExternalStore( + const phase = useSyncExternalStore( feed.subscribe, () => feed.getSnapshot().get(sessionId)?.hostExecutionPhase ?? null, () => null ) + const childKey = useSyncExternalStore( + feed.subscribe, + () => { + const child = feed.getSnapshot().get(sessionId)?.hostExecutionChild + return child?.generation ?? child?.fence ?? null + }, + () => null + ) + return { phase, childKey } } function projectStatus( diff --git a/src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts b/src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts index 5e6fecefa2f..3e760a787ca 100644 --- a/src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts @@ -10,6 +10,7 @@ import { NATIVE_CHAT_RAIL_MAX_TICKS, type NativeChatRailItem } from './native-chat-message-rail-items' +import { selectNativeChatActiveTurnKey } from '../../../../shared/native-chat-turn-status' function text(id: string, body: string, role: NativeChatMessage['role'] = 'assistant') { return { @@ -42,7 +43,7 @@ function slotsOf(messages: NativeChatMessage[]) { return buildNativeChatTranscriptSlots({ messages, turnKeys, - latestUserIndex: messages.findLastIndex((message) => message.role === 'user'), + activeTurnKey: selectNativeChatActiveTurnKey(messages), currentTurnKey: undefined, receipts: new Map(), turnStatuses: { active: null, completedByTurn: {} }, diff --git a/src/renderer/src/components/native-chat/native-chat-rail-outline-parity.test.ts b/src/renderer/src/components/native-chat/native-chat-rail-outline-parity.test.ts index 385899ae91f..5ee40345e1b 100644 --- a/src/renderer/src/components/native-chat/native-chat-rail-outline-parity.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-rail-outline-parity.test.ts @@ -18,6 +18,7 @@ import { omitNativeChatThreadGoalRows } from './native-chat-thread-goal-rows' import { buildNativeChatTranscriptSlots } from './native-chat-transcript-slots' import type { NativeChatTurnDiff } from './native-chat-turn-diffs' import { projectStructuredAgentSessionMessages } from './structured-agent-session-message-projection' +import { selectNativeChatActiveTurnKey } from '../../../../shared/native-chat-turn-status' function row(sequence: number, body: AgentJournalItemBody, itemId = `item-${sequence}`) { return { itemId, revision: 1, sequence, observedAt: 1_000 + sequence, body } @@ -81,7 +82,7 @@ function loadedRailItems(items: AgentJournalRenderItem[], submissions: AgentJour const slots = buildNativeChatTranscriptSlots({ messages, turnKeys, - latestUserIndex: messages.findLastIndex((message) => message.role === 'user'), + activeTurnKey: selectNativeChatActiveTurnKey(messages), currentTurnKey: turn, receipts: new Map(), turnStatuses: { active: null, completedByTurn: {} }, diff --git a/src/renderer/src/components/native-chat/native-chat-row-height-estimate.ts b/src/renderer/src/components/native-chat/native-chat-row-height-estimate.ts index 60075385292..368eea2699b 100644 --- a/src/renderer/src/components/native-chat/native-chat-row-height-estimate.ts +++ b/src/renderer/src/components/native-chat/native-chat-row-height-estimate.ts @@ -29,6 +29,8 @@ export type NativeChatRowChromeMetrics = { /** Behind a folded turn: prose and tool activity draw nothing, so estimating * them would reserve a screen of height for a row that paints a roster. */ folded?: boolean + /** A subagent wrote the row, so it draws a caption naming that subagent. */ + attributed?: boolean } const LINE_HEIGHT_PX = 22 @@ -38,6 +40,7 @@ const USER_BUBBLE_CHROME_PX = 32 const IMAGE_STRIP_PX = 88 const TOOL_RUN_PX = 40 const SUBAGENT_ROW_PX = 32 +const SUBAGENT_CAPTION_PX = 24 const STATUS_ROW_PX = 28 const TURN_DIFF_PX = 28 const RECEIPT_PX = 56 @@ -117,6 +120,9 @@ export function estimateNativeChatRowHeight( height += TOOL_RUN_PX } height += content.subagentGroupCount * SUBAGENT_ROW_PX + if (chrome.attributed === true && height > 0) { + height += SUBAGENT_CAPTION_PX + } partCount = height > 0 ? 1 : 0 } if (chrome.hasStatus) { diff --git a/src/renderer/src/components/native-chat/native-chat-tool-fold.test.ts b/src/renderer/src/components/native-chat/native-chat-tool-fold.test.ts index 2e15d24eb2a..3c83b6a2796 100644 --- a/src/renderer/src/components/native-chat/native-chat-tool-fold.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-tool-fold.test.ts @@ -252,3 +252,57 @@ describe('spawn-group roster rows', () => { expect(prose.map((block) => block.type)).toEqual(['text', 'subagent-group']) }) }) + +describe('foldToolMessages — each agent folds into its own run', () => { + const child = ( + overrides: Partial & Pick + ): NativeChatMessage => msg({ agentId: 'task-1', ...overrides }) + const text = (value: string) => ({ type: 'text' as const, text: value }) + const call = (name: string) => ({ type: 'tool-call' as const, name, input: {} }) + + it("does not fold a subagent's tool calls into its parent's message", () => { + const folded = foldToolMessages([ + msg({ id: 'parent', blocks: [text('delegating')] }), + child({ id: 'child-grep', blocks: [call('Grep')] }) + ]) + expect(folded.map((message) => message.id)).toEqual(['parent', 'child-grep']) + expect(folded[0]?.blocks).toEqual([text('delegating')]) + }) + + it("keeps interleaved agents in order, each call in its own agent's run", () => { + const folded = foldToolMessages([ + msg({ id: 'parent', blocks: [text('delegating')] }), + child({ id: 'child', blocks: [text('looking')] }), + child({ id: 'child-grep', blocks: [call('Grep')] }), + msg({ id: 'parent-read', blocks: [call('Read')] }) + ]) + // The parent's later call stays where it happened, below the child's work, + // rather than jumping back up into the parent's earlier row. + expect(folded.map((message) => message.id)).toEqual(['parent', 'child', 'parent-read']) + expect(folded[0]?.blocks).toEqual([text('delegating')]) + expect(folded[1]?.blocks).toEqual([text('looking'), call('Grep')]) + expect(folded[1]?.agentId).toBe('task-1') + expect(folded[2]?.agentId).toBeUndefined() + }) + + it("ends every agent's run at a turn boundary", () => { + const folded = foldToolMessages([ + child({ id: 'child', blocks: [text('looking')] }), + msg({ id: 'ask', role: 'user', blocks: [text('next')] }), + child({ id: 'child-grep', blocks: [call('Grep')] }) + ]) + // The child's later call stays in the turn it happened in. + expect(folded.map((message) => message.id)).toEqual(['child', 'ask', 'child-grep']) + }) + + it('folds a transcript that names no producer exactly as a single run', () => { + const folded = foldToolMessages([ + msg({ id: 'a', blocks: [text('one')] }), + msg({ id: 'a-call', blocks: [call('Bash')] }), + msg({ id: 'notice', role: 'system', blocks: [text('Something happened')] }), + msg({ id: 'b-call', blocks: [call('Read')] }) + ]) + expect(folded.map((message) => message.id)).toEqual(['a', 'notice', 'b-call']) + expect(folded[0]?.blocks).toEqual([text('one'), call('Bash')]) + }) +}) diff --git a/src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts b/src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts index f22d25bfa8f..5f1fc09b3a1 100644 --- a/src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import type { NativeChatMessage } from '../../../../shared/native-chat-types' import { + selectNativeChatActiveTurnKey, selectNativeChatTurnStatuses, type NativeChatTurnStatus } from '../../../../shared/native-chat-turn-status' @@ -38,7 +39,7 @@ function build( return buildNativeChatTranscriptSlots({ messages, turnKeys, - latestUserIndex: messages.findLastIndex((message) => message.role === 'user'), + activeTurnKey: selectNativeChatActiveTurnKey(messages), currentTurnKey: undefined, receipts: new Map(), turnStatuses: NO_STATUSES, @@ -112,7 +113,7 @@ describe('transcript slots', () => { it('keeps a message whose only content is a turn status under it', () => { const status: NativeChatTurnStatus = { startedAt: 1, thinking: false, workedSeconds: 4 } const slots = build([text('u', '', 'user')], { - latestUserIndex: 0, + activeTurnKey: 'u', turnStatuses: { active: status, completedByTurn: {} } }) expect(slots).toHaveLength(1) @@ -142,7 +143,7 @@ describe('transcript slots', () => { it('puts the running turn bar under the prompt it answers', () => { const status: NativeChatTurnStatus = { startedAt: 1, thinking: false, workedSeconds: null } const slots = build([text('u', 'ask', 'user'), text('a', 'answer')], { - latestUserIndex: 0, + activeTurnKey: 'u', turnStatuses: { active: status, completedByTurn: {} }, isWorking: true }) @@ -206,3 +207,72 @@ describe('a send the host rejected', () => { ]) }) }) + +describe("a subagent's rows speak as that subagent", () => { + const settled: NativeChatTurnStatus = { startedAt: 1, thinking: false, workedSeconds: 5 } + const roster: NativeChatMessage = { + id: 'roster', + role: 'system', + blocks: [ + { + type: 'subagent-group', + groupId: 'group-1', + agents: [{ id: 'task-1', label: 'explore the lane', state: 'working' }] + } + ], + timestamp: 1, + source: 'transcript' + } + const child = (id: string, body: string, agentId = 'task-1'): NativeChatMessage => ({ + ...text(id, body), + agentId + }) + const messages = [ + text('ask', 'summarise the repo', 'user'), + text('answer', 'Delegated; the summary follows.'), + roster, + child('child-said', 'The PR is CLEAN.') + ] + + it("keeps the parent's answer on a settled turn and folds the subagent's later words", () => { + const slots = build(messages, { + turnStatuses: { active: null, completedByTurn: { ask: settled } }, + subagentLabels: new Map([['task-1', 'explore the lane']]) + }) + const drawn = slots.filter((slot) => !slot.folded).map((slot) => slot.message.id) + expect(drawn).toContain('answer') + expect(drawn).not.toContain('child-said') + }) + + it("names the subagent on its row from the roster, and only on a subagent's row", () => { + const slots = build(messages, { + subagentLabels: new Map([['task-1', 'explore the lane']]) + }) + const labelOf = (id: string) => slots.find((slot) => slot.message.id === id)?.subagentLabel + expect(labelOf('child-said')).toBe('explore the lane') + expect(labelOf('answer')).toBeUndefined() + }) + + it("keeps the parent's run live while its subagent works below it", () => { + const trailing = (rows: NativeChatMessage[]) => + build(rows) + .filter((slot) => slot.trailingRun) + .map((slot) => slot.message.id) + const childRun: NativeChatMessage = { ...toolRun('child-run'), agentId: 'task-1' } + // The parent is still inside its spawn call; the child's work does not move it past. + expect(trailing([text('ask', 'go', 'user'), toolRun('spawn'), childRun])).toEqual([ + 'spawn', + 'child-run' + ]) + // The parent answering does move it past its own run, whatever the child does. + expect( + trailing([text('ask', 'go', 'user'), toolRun('spawn'), text('said', 'Done.'), childRun]) + ).toEqual(['said', 'child-run']) + }) + + it('reserves room for the caption on a subagent row', () => { + const [parentSlot] = build([text('mine', 'same words')]) + const [childSlot] = build([child('theirs', 'same words')]) + expect(childSlot!.estimatedHeight).toBeGreaterThan(parentSlot!.estimatedHeight) + }) +}) diff --git a/src/renderer/src/components/native-chat/native-chat-transcript-slots.ts b/src/renderer/src/components/native-chat/native-chat-transcript-slots.ts index a0fe23b6df3..6e0dc5bff4b 100644 --- a/src/renderer/src/components/native-chat/native-chat-transcript-slots.ts +++ b/src/renderer/src/components/native-chat/native-chat-transcript-slots.ts @@ -13,6 +13,8 @@ import { isToolCallBlock, type NativeChatMessage } from '../../../../shared/native-chat-types' +import { agentJournalItemSubagentId } from '../../../../shared/agent-session-journal-producer' +import { nativeChatSubagentLabel } from '../../../../shared/native-chat-subagent-attribution' import type { NativeChatTurnStatus } from '../../../../shared/native-chat-turn-status' import { nativeChatTurnFold, @@ -48,6 +50,9 @@ export type NativeChatTranscriptSlot = { /** Whether this row's turn hides anything, so its status row offers a caret. */ turnFolds: boolean turnDiff: NativeChatTurnDiff | undefined + /** The roster's name for the subagent that wrote this row, when one names it. + * Whether a subagent wrote it at all is the message's own linkage. */ + subagentLabel: string | undefined /** Height to reserve before the row has ever been measured. */ estimatedHeight: number } @@ -55,7 +60,8 @@ export type NativeChatTranscriptSlot = { export type NativeChatTranscriptSlotsInput = { messages: readonly NativeChatMessage[] turnKeys: readonly (string | undefined)[] - latestUserIndex: number + /** The user message whose bar carries the live turn's status. */ + activeTurnKey: string currentTurnKey: string | undefined receipts: ReadonlyMap turnStatuses: { @@ -69,6 +75,8 @@ export type NativeChatTranscriptSlotsInput = { isWorking: boolean /** Session-level lifecycle, which outlives a transcript that never said "done". */ lifecycleWorking: boolean + /** Each subagent's roster label, by the id its rows carry. */ + subagentLabels?: ReadonlyMap } export function buildNativeChatTranscriptSlots( @@ -77,7 +85,7 @@ export function buildNativeChatTranscriptSlots( const { messages, turnKeys, - latestUserIndex, + activeTurnKey, currentTurnKey, receipts, turnStatuses, @@ -85,12 +93,14 @@ export function buildNativeChatTranscriptSlots( showTurnStatus, expandedTurnKeys, isWorking, - lifecycleWorking + lifecycleWorking, + subagentLabels } = input // One pass to decide what each row draws, then the fold over those readings — // so "is this the answer" and "does this row render prose" cannot disagree. const foldRows: NativeChatTurnFoldRow[] = messages.map((message, index) => { const content = deriveNativeChatRowContent(message.blocks) + const agentId = agentJournalItemSubagentId(message) return { turnKey: turnKeys[index], role: message.role, @@ -99,20 +109,32 @@ export function buildNativeChatTranscriptSlots( // and its plain-text twin is then the only record the spawn happened. outlivesTurn: message.blocks.some( (block) => isSubagentGroupBlock(block) || isBackgroundTaskBlock(block) - ) + ), + ...(agentId === null ? {} : { agentId }) } }) // Liveness is the turn's, not any one call's: the run at the frontier stays // live between its calls, and a run the agent has moved past is settled even // while its last call is still reporting. An approval's receipt decides a call // of the run above it, which then runs, so it does not move past that run. - const trailingRunIndex = foldRows.findLastIndex( - (row, index) => + // Each agent has its own frontier: a subagent working below its parent's run + // has not moved the parent past it. + const trailingRunIndexes = new Set() + const agentsWithFrontier = new Set() + for (let index = foldRows.length - 1; index >= 0; index -= 1) { + const row = foldRows[index]! + const agent = agentJournalItemSubagentId(messages[index]) + if ( + !agentsWithFrontier.has(agent) && row.role !== 'user' && row.role !== 'reasoning' && receipts.get(messages[index].id)?.kind !== 'approval' && (row.rendersProse || messages[index].blocks.some(isToolCallBlock)) - ) + ) { + trailingRunIndexes.add(index) + agentsWithFrontier.add(agent) + } + } const settledTurnKeys = new Set( showTurnStatus ? Object.entries(turnStatuses.completedByTurn) @@ -130,7 +152,7 @@ export function buildNativeChatTranscriptSlots( const turnKey = turnKeys[index] const receipt = receipts.get(message.id) const candidateStatus = - index === latestUserIndex + message.role === 'user' && message.id === activeTurnKey ? turnStatuses.active : message.role === 'user' && turnKey ? turnStatuses.completedByTurn[turnKey] @@ -147,23 +169,27 @@ export function buildNativeChatTranscriptSlots( if (!drawsRow && status === undefined && turnDiff === undefined) { continue } + const subagentId = agentJournalItemSubagentId(message) slots.push({ message, turnKey, activeTurnIsWorking: (currentTurnKey ? turnKey === currentTurnKey : turnKey === undefined) && (isWorking || lifecycleWorking), - trailingRun: index === trailingRunIndex, + trailingRun: trailingRunIndexes.has(index), receipt, status: status ?? undefined, folded, turnFolds: turnKey !== undefined && foldableTurnKeys.has(turnKey), turnDiff, + subagentLabel: nativeChatSubagentLabel(subagentLabels, message), estimatedHeight: estimateNativeChatRowHeight(nativeChatRowContentMetrics(message), { hasReceipt: receipt !== undefined, hasStatus: status !== undefined, hasTurnDiff: turnDiff !== undefined, - folded + folded, + // Only an agent's own row draws the caption; a receipt stands in for it. + attributed: subagentId !== null && receipt === undefined && message.role !== 'user' }) }) } diff --git a/src/renderer/src/components/native-chat/structured-agent-question-projection.test.ts b/src/renderer/src/components/native-chat/structured-agent-question-projection.test.ts index ab74c2efef2..09154a2512a 100644 --- a/src/renderer/src/components/native-chat/structured-agent-question-projection.test.ts +++ b/src/renderer/src/components/native-chat/structured-agent-question-projection.test.ts @@ -215,3 +215,19 @@ describe('structured agent session ask-row projection', () => { ).toHaveLength(1) }) }) + +describe("a subagent's pending question keeps its producer", () => { + it('carries the linkage onto the row that stands in for the question', () => { + const projected = projectQuestion({ + ...item('child-q', { + kind: 'question', + question: 'Which branch?', + options: [{ id: 'q1:main', label: 'main' }], + resolution: { ...PENDING } + }), + agentId: 'task-1', + producerKind: 'agent' + }) + expect(projected).toMatchObject({ role: 'system', agentId: 'task-1' }) + }) +}) diff --git a/src/renderer/src/components/native-chat/structured-agent-question-projection.ts b/src/renderer/src/components/native-chat/structured-agent-question-projection.ts index 9469c443d0a..da247876119 100644 --- a/src/renderer/src/components/native-chat/structured-agent-question-projection.ts +++ b/src/renderer/src/components/native-chat/structured-agent-question-projection.ts @@ -5,6 +5,7 @@ import type { import { isAskUserQuestionTool } from '../../../../shared/agent-question-answered-intent' import { parseAskFromToolInput } from '../../../../shared/native-chat-ask' import { agentJournalItemRowOrigin } from '../../../../shared/agent-session-journal-position' +import { agentJournalLinkageFields } from '../../../../shared/agent-session-journal-producer' import type { NativeChatMessage } from '../../../../shared/native-chat-types' import { projectStructuredItemToNativeChat } from '../../../../shared/structured-agent-session-projection' import { readAgentJournalTurn } from '../../../../shared/agent-session-turn-record' @@ -71,6 +72,7 @@ function projectItem(item: AgentJournalRenderItem): Projection { // A system row preserves question identity through tool folding; the receipt renders its body. message = { ...agentJournalItemRowOrigin(item), + ...agentJournalLinkageFields(item), role: 'system', blocks: [{ type: 'text', text: body.question }] } diff --git a/src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts b/src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts index cec8d63a9c7..b48025533f4 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts @@ -8,6 +8,7 @@ import type { NativeChatResolvedPrompt } from './native-chat-resolution-receipt' import type { NativeChatTurnDiff } from './native-chat-turn-diffs' import { buildNativeChatTranscriptSlots } from './native-chat-transcript-slots' import { useNativeChatMessageRail } from './use-native-chat-message-rail' +import { selectNativeChatActiveTurnKey } from '../../../../shared/native-chat-turn-status' function message(id: string, role: NativeChatMessage['role']): NativeChatMessage { return { @@ -31,7 +32,7 @@ function slotsOf(messages: NativeChatMessage[]) { return buildNativeChatTranscriptSlots({ messages, turnKeys, - latestUserIndex: messages.findLastIndex((entry) => entry.role === 'user'), + activeTurnKey: selectNativeChatActiveTurnKey(messages), currentTurnKey: undefined, receipts: new Map(), turnStatuses: { active: null, completedByTurn: {} }, diff --git a/src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx index 823f78af6ed..7d245cc76fd 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx @@ -58,6 +58,7 @@ function slot(id: string): NativeChatTranscriptSlot { folded: false, turnFolds: false, turnDiff: undefined, + subagentLabel: undefined, estimatedHeight: 48 } } diff --git a/src/renderer/src/components/native-chat/use-native-chat-turn-status.ts b/src/renderer/src/components/native-chat/use-native-chat-turn-status.ts index 82d98c77ec0..95db05141d2 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-turn-status.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-turn-status.ts @@ -12,14 +12,15 @@ export type { NativeChatTurnStatus } export function useNativeChatTurnStatus({ messages, - latestUserIndex, + activeTurnKey, isWorking, workingStartedAt, settledTurns, thinking = false }: { messages: readonly NativeChatMessage[] - latestUserIndex: number + /** The user message whose bar carries the live clock (`selectNativeChatActiveTurnKey`). */ + activeTurnKey: string isWorking: boolean workingStartedAt?: number | null /** Host-recorded durations; they outrank whatever this client observed. */ @@ -30,8 +31,6 @@ export function useNativeChatTurnStatus({ active: NativeChatTurnStatus | null completedByTurn: Readonly> } { - const latestUserId = latestUserIndex !== -1 ? (messages[latestUserIndex]?.id ?? null) : null - const activeTurnKey = latestUserId ?? '__unanchored__' const [timingByTurn, setTimingByTurn] = useState({}) useLayoutEffect(() => { diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session.ts b/src/renderer/src/components/native-chat/use-structured-agent-session.ts index 659bbafbbd9..d9ca4995b13 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session.ts +++ b/src/renderer/src/components/native-chat/use-structured-agent-session.ts @@ -156,6 +156,7 @@ export function useStructuredAgentSession(args: { isWorking: transportState.isWorking, workingStartedAt: transportState.turnTiming.workingStartedAt, settledTurns: transportState.turnTiming.settledTurns, + activeTurnOpenedBy: transportState.turnTiming.activeTurnOpenedBy, turnActivity: transportState.turnActivity, backgroundTasks: transportState.backgroundTasks, turnId: transportState.turnId, diff --git a/src/renderer/src/components/native-chat/use-structured-agent-turn-timing.ts b/src/renderer/src/components/native-chat/use-structured-agent-turn-timing.ts index 5603c82b42f..1c26fd9750c 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-turn-timing.ts +++ b/src/renderer/src/components/native-chat/use-structured-agent-turn-timing.ts @@ -5,18 +5,16 @@ import type { } from '../../../../shared/agent-session-journal-types' import type { NativeChatSettledTurns } from '../../../../shared/native-chat-turn-status' import type { StructuredAgentHostClock } from '../../../../shared/structured-agent-session-reducer' -import { - selectStructuredAgentRunningTurnTiming, - selectStructuredAgentSettledTurns -} from '../../../../shared/structured-agent-session-turn-timing' +import { selectStructuredAgentTurnBars } from '../../../../shared/structured-agent-session-turn-timing' import { stepStructuredAgentTurnClock, type StructuredAgentTurnClockLatch } from '../../../../shared/structured-agent-turn-clock-anchor' /** Host-recorded turn timing for the structured lane: settled durations straight - * off the journal, and a skew-free start for the live counter whose host-to-local - * conversion is latched once per turn. */ + * off the journal, the user message that owns the running turn's bar, and a + * skew-free start for the live counter whose host-to-local conversion is latched + * once per turn. */ export function useStructuredAgentTurnTiming( { items, @@ -28,16 +26,16 @@ export function useStructuredAgentTurnTiming( hostClock?: StructuredAgentHostClock | null }, turnId: string | null -): { settledTurns: NativeChatSettledTurns; workingStartedAt: number | null } { - const settledTurns = useMemo( - () => selectStructuredAgentSettledTurns(items, submissions), - [items, submissions] +): { + settledTurns: NativeChatSettledTurns + workingStartedAt: number | null + activeTurnOpenedBy: string | null +} { + const { settledTurns, runningTiming, activeTurnOpenedBy } = useMemo( + () => selectStructuredAgentTurnBars(items, submissions, turnId), + [items, submissions, turnId] ) const [latch, setLatch] = useState(null) - const runningTiming = useMemo( - () => (turnId === null ? null : selectStructuredAgentRunningTurnTiming(items, turnId)), - [items, turnId] - ) // Stamp during render (React's derive-from-props pattern) so the first paint of // a new turn already counts from the right instant. const step = stepStructuredAgentTurnClock({ @@ -50,5 +48,5 @@ export function useStructuredAgentTurnTiming( if (step.latch !== latch) { setLatch(step.latch) } - return { settledTurns, workingStartedAt: step.workingStartedAt } + return { settledTurns, workingStartedAt: step.workingStartedAt, activeTurnOpenedBy } } diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx index f7b18f6ebaa..55deb82b95b 100644 --- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx +++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx @@ -272,7 +272,7 @@ export function NewWorkspaceComposerAdvancedSection({

@@ -282,12 +282,6 @@ export function NewWorkspaceComposerAdvancedSection({ 'Wait for setup to complete before starting agent' )} - - {translate( - 'auto.components.NewWorkspaceComposerCard.waitForSetupBeforeAgentHelp', - 'Turn this on when setup installs dependencies, MCP servers, or config files the agent needs during startup.' - )} - - -function readFunctionName(node: ts.Node): string | null { - if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) && node.name) { - return node.name.text - } - if ( - ts.isArrowFunction(node) && - ts.isVariableDeclaration(node.parent) && - ts.isIdentifier(node.parent.name) - ) { - return node.parent.name.text - } - return null -} - -function readHookName(node: ts.CallExpression): string | null { - if (ts.isIdentifier(node.expression) && /^use[A-Z]/.test(node.expression.text)) { - return node.expression.text - } - if ( - ts.isPropertyAccessExpression(node.expression) && - /^use[A-Z]/.test(node.expression.name.text) - ) { - return node.expression.name.text - } - return null -} - -function normalize(node: ts.Node, sourceFile: ts.SourceFile): string { - return node - .getText(sourceFile) - .replace(/\/\*[\s\S]*?\*\//g, '') - .replace(/\/\/.*$/gm, '') - .replace(/\s+/g, '') -} - -export function readHookDefinitions( - sources: readonly HookSource[], - includesDefinition: (name: string) => boolean -): HookDefinitions { - const definitions = new Map() - for (const { relativePath, source } of sources) { - const sourceFile = ts.createSourceFile( - relativePath, - source, - ts.ScriptTarget.Latest, - true, - relativePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS - ) - const visit = (node: ts.Node): void => { - if ( - (ts.isFunctionDeclaration(node) || - ts.isFunctionExpression(node) || - ts.isArrowFunction(node)) && - node.body - ) { - const name = readFunctionName(node) - if (name && includesDefinition(name)) { - definitions.set(name, { body: node.body, sourceFile }) - } - } - ts.forEachChild(node, visit) - } - visit(sourceFile) - } - return definitions -} - -export function readFlattenedHookFacts( - definitions: HookDefinitions, - rootName: string -): HookContractFact[] { - const flatten = (name: string, active: ReadonlySet): HookContractFact[] => { - const definition = definitions.get(name) - if (!definition || active.has(name)) { - throw new Error(`Invalid hook stage: ${name}`) - } - const facts: HookContractFact[] = [] - const nextActive = new Set([...active, name]) - const visit = (node: ts.Node): void => { - if (ts.isCallExpression(node)) { - const hookName = readHookName(node) - if (hookName) { - if (definitions.has(hookName)) { - facts.push(...flatten(hookName, nextActive)) - } else { - const dependencyArray = [...node.arguments] - .toReversed() - .find((argument) => ts.isArrayLiteralExpression(argument)) - facts.push({ - dependencies: dependencyArray - ? [...dependencyArray.elements].map((element) => - normalize(element, definition.sourceFile) - ) - : null, - name: hookName - }) - } - } - } - ts.forEachChild(node, visit) - } - visit(definition.body) - return facts - } - - return flatten(rootName, new Set()) -} - -export function hashContract(values: readonly string[]): string { - return createHash('sha256').update(values.join('\n')).digest('hex') -} - -export function readDependencyContract(facts: readonly HookContractFact[]): string[] { - return facts.flatMap(({ dependencies, name }) => - dependencies === null ? [] : [`${name}|[${dependencies.join(',')}]`] - ) -} - -export function projectDependencyContract( - facts: readonly HookContractFact[], - allowedAdditions: Readonly> -): string[] { - const dependencyFacts = facts.filter( - (fact): fact is HookContractFact & { dependencies: string[] } => fact.dependencies !== null - ) - for (const ordinal of Object.keys(allowedAdditions).map(Number)) { - if (!Number.isInteger(ordinal) || ordinal < 0 || ordinal >= dependencyFacts.length) { - throw new Error(`Invalid dependency-array ordinal: ${ordinal}`) - } - } - return dependencyFacts.map(({ dependencies, name }, ordinal) => { - const projected = [...dependencies] - for (const addition of allowedAdditions[ordinal] ?? []) { - const index = projected.indexOf(addition) - if (index === -1) { - throw new Error(`Missing allowed dependency addition at ${ordinal}: ${addition}`) - } - projected.splice(index, 1) - } - return `${name}|[${projected.join(',')}]` - }) -} diff --git a/src/renderer/src/components/settings/RepositoryHookPolicySettings.tsx b/src/renderer/src/components/settings/RepositoryHookPolicySettings.tsx index a6bedb5949c..6a41bdb974e 100644 --- a/src/renderer/src/components/settings/RepositoryHookPolicySettings.tsx +++ b/src/renderer/src/components/settings/RepositoryHookPolicySettings.tsx @@ -141,19 +141,13 @@ export function RepositorySetupPolicySetting({ />
-
+
{translate( 'auto.components.settings.RepositoryHooksSection.waitForSetupBeforeAgent', 'Wait for setup to complete before starting agent' )}
-

- {translate( - 'auto.components.settings.RepositoryHooksSection.waitForSetupBeforeAgentHelp', - 'Turn this on when setup installs dependencies, MCP servers, or config files the agent needs during startup.' - )} -

{ ]) }) - it('keeps other levels when only the collapsed width moves', () => { - // Collapsing different chips changes the fit width, never the natural one. - expect(recordStatusBarDensityWidth([w(700), w(600, 450)], 1, w(600, 430))).toEqual([ - w(700), - w(600, 430) - ]) + it('drops only the other collapsing levels when only the collapsed width moves', () => { + // Urgency changes which chips collapse, moving fit widths but never the natural one. + expect( + recordStatusBarDensityWidth( + [w(700), w(600), w(500), w(600, 450), w(500, 400)], + 3, + w(600, 430) + ) + ).toEqual([w(700), w(600), w(500), w(600, 430), undefined]) + }) + + it('re-probes a roomier collapsing level once the tightest level sees its fit move', () => { + // Level 3's stale fit (450) kept the bar at level 4; fewer urgent chips now let it fit. + const settled = [w(700), w(600), w(500), w(600, 450), w(500, 400)] + expect(pickStatusBarDensityLevel(settled, 420)).toBe(TIGHTEST) + const remeasured = recordStatusBarDensityWidth(settled, TIGHTEST, w(500, 380)) + expect(remeasured).toEqual([w(700), w(600), w(500), undefined, w(500, 380)]) + expect(pickStatusBarDensityLevel(remeasured, 420)).toBe(3) + const probed = recordStatusBarDensityWidth(remeasured, 3, w(600, 410)) + expect(pickStatusBarDensityLevel(probed, 420)).toBe(3) }) it('keeps other levels on a sub-pixel re-measure', () => { diff --git a/src/renderer/src/components/status-bar/status-bar-density.ts b/src/renderer/src/components/status-bar/status-bar-density.ts index 17748c19d45..6801d910358 100644 --- a/src/renderer/src/components/status-bar/status-bar-density.ts +++ b/src/renderer/src/components/status-bar/status-bar-density.ts @@ -56,9 +56,17 @@ export function recordStatusBarDensityWidth( width: StatusBarLevelWidth ): (StatusBarLevelWidth | undefined)[] { const previous = levelWidths[level] - const next = + const naturalChanged = previous !== undefined && Math.abs(previous.natural - width.natural) > WIDTH_TOLERANCE_PX - ? [] + // Why: urgency can change which chips pin without moving the natural width. + const fitChanged = + previous !== undefined && Math.abs(previous.fit - width.fit) > WIDTH_TOLERANCE_PX + const next = naturalChanged + ? [] + : fitChanged + ? levelWidths.map((entry, index) => + STATUS_BAR_DENSITY_LEVELS[index]?.collapseUsage ? undefined : entry + ) : [...levelWidths] next[level] = width return next diff --git a/src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx b/src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx index 964214e0d9f..ea71e0e8fd5 100644 --- a/src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx +++ b/src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx @@ -2,10 +2,13 @@ import { act, cleanup, fireEvent, render } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { RetainedPaneHost } from './RetainedPaneHost' +import { registerTabGroupBody } from './tab-group-body-geometry' const disconnect = vi.fn() +const observerCreated = vi.fn() let notifyResize: () => void let anchors: HTMLDivElement[] +let unregisterAnchors: (() => void)[] beforeEach(() => { vi.stubGlobal('__ORCA_WEB_CLIENT__', true) @@ -14,12 +17,15 @@ beforeEach(() => { class { constructor(callback: () => void) { notifyResize = callback + observerCreated() } observe(): void {} + unobserve(): void {} disconnect = disconnect } ) disconnect.mockClear() + observerCreated.mockClear() anchors = ['left', 'right'].map((id, index) => { const anchor = document.createElement('div') anchor.dataset.tabGroupBodyId = id @@ -27,11 +33,14 @@ beforeEach(() => { document.body.append(anchor) return anchor }) + unregisterAnchors = ['left', 'right'].map((id, index) => registerTabGroupBody(id, anchors[index])) }) afterEach(() => { cleanup() + unregisterAnchors.forEach((unregister) => unregister()) anchors.forEach((anchor) => anchor.remove()) + vi.restoreAllMocks() vi.unstubAllGlobals() }) @@ -105,3 +114,84 @@ it('allows hidden terminal startup measurement without exposing input or startin expect(timeout).not.toHaveBeenCalled() timeout.mockRestore() }) + +function countResizeListeners(spy: ReturnType): number { + return spy.mock.calls.filter(([type]) => type === 'resize').length +} + +it('measures a group once for all of its visible panes', () => { + const addListener = vi.spyOn(window, 'addEventListener') + const view = render( + <> + + + + + + + + + + + ) + expect(observerCreated).toHaveBeenCalledTimes(1) + expect(countResizeListeners(addListener)).toBe(1) + + const measureBody = vi.fn(() => new DOMRect(0, 40, 300, 500)) + anchors[0].getBoundingClientRect = measureBody + act(() => notifyResize()) + expect(measureBody).toHaveBeenCalledTimes(1) + const hosts = Array.from( + view.container.querySelectorAll('[data-retained-pane-host]') + ) + expect(hosts.map((host) => host.style.width)).toEqual(['300px', '300px', '300px']) + + const removeListener = vi.spyOn(window, 'removeEventListener') + view.unmount() + expect(disconnect).toHaveBeenCalledTimes(1) + expect(countResizeListeners(removeListener)).toBe(1) +}) + +it('does not measure a hidden pane until it becomes visible', () => { + const addListener = vi.spyOn(window, 'addEventListener') + const view = render( + + + + ) + const host = view.container.firstElementChild as HTMLDivElement + expect(observerCreated).not.toHaveBeenCalled() + expect(countResizeListeners(addListener)).toBe(0) + expect(host.style.display).toBe('none') + + anchors[0].getBoundingClientRect = () => new DOMRect(0, 48, 320, 400) + view.rerender( + + + + ) + expect(observerCreated).toHaveBeenCalledTimes(1) + expect(host.style.top).toBe('48px') + expect(host.style.width).toBe('320px') + + view.rerender( + + + + ) + expect(disconnect).toHaveBeenCalledTimes(1) +}) + +it('keeps measuring a hidden pane that asks to measure while hidden', () => { + const view = render( + + + + ) + const host = view.container.firstElementChild as HTMLDivElement + expect(observerCreated).toHaveBeenCalledTimes(1) + expect(host.style.width).toBe('400px') + anchors[0].getBoundingClientRect = () => new DOMRect(0, 32, 360, 568) + act(() => notifyResize()) + expect(host.style.width).toBe('360px') +}) diff --git a/src/renderer/src/components/tab-group/RetainedPaneHost.tsx b/src/renderer/src/components/tab-group/RetainedPaneHost.tsx index 6007f2b1c20..c9ec5d3a826 100644 --- a/src/renderer/src/components/tab-group/RetainedPaneHost.tsx +++ b/src/renderer/src/components/tab-group/RetainedPaneHost.tsx @@ -1,6 +1,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react' import { SYNC_FIT_PANES_EVENT } from '@/constants/terminal' import { tabGroupBodyAnchorName } from './tab-group-body-anchor' +import { subscribeTabGroupBodyRect, type TabGroupBodyRect } from './tab-group-body-geometry' const HAS_CSS_ANCHOR_POSITIONING = typeof CSS !== 'undefined' && @@ -9,7 +10,6 @@ const HAS_CSS_ANCHOR_POSITIONING = CSS.supports('width', 'anchor-size(--orca-terminal-overlay-probe width)') const MIN_OVERLAY_FIT_WIDTH_PX = 48 const MIN_OVERLAY_FIT_HEIGHT_PX = 24 -const FALLBACK_RECT_MIN_CHANGE_PX = 1 function shouldUseCssAnchorPositioning(): boolean { return ( @@ -18,13 +18,6 @@ function shouldUseCssAnchorPositioning(): boolean { ) } -type MeasuredFallbackRect = { - top: number - left: number - width: number - height: number -} - type RetainedPaneHostProps = { groupId: string | undefined isVisible: boolean @@ -47,67 +40,17 @@ export function RetainedPaneHost({ }: RetainedPaneHostProps): React.JSX.Element { const anchorName = groupId !== undefined ? tabGroupBodyAnchorName(groupId) : undefined const overlayRef = useRef(null) - const [measuredFallbackRect, setMeasuredFallbackRect] = useState( - null - ) + const [measuredFallbackRect, setMeasuredFallbackRect] = useState(null) + // Why: a hidden host renders display:none, so it has no use for group geometry until shown. + const measuresFallbackRect = + groupId !== undefined && !shouldUseCssAnchorPositioning() && (isVisible || measureWhileHidden) useLayoutEffect(() => { - if (!anchorName || shouldUseCssAnchorPositioning() || !groupId) { + const container = overlayRef.current?.parentElement + if (!measuresFallbackRect || groupId === undefined || !container) { return } - - const findBody = (): HTMLElement | null => { - for (const candidate of document.querySelectorAll('[data-tab-group-body-id]')) { - if (candidate.dataset.tabGroupBodyId === groupId) { - return candidate - } - } - return null - } - - const updateRect = (): void => { - const overlay = overlayRef.current - const parent = overlay?.parentElement - const body = findBody() - if (!parent || !body) { - setMeasuredFallbackRect(null) - return - } - const parentRect = parent.getBoundingClientRect() - const bodyRect = body.getBoundingClientRect() - const next: MeasuredFallbackRect = { - top: bodyRect.top - parentRect.top, - left: bodyRect.left - parentRect.left, - width: bodyRect.width, - height: bodyRect.height - } - // Why: ResizeObserver and xterm fit can otherwise amplify sub-pixel jitter forever. - setMeasuredFallbackRect((prev) => - prev && - Math.abs(prev.top - next.top) < FALLBACK_RECT_MIN_CHANGE_PX && - Math.abs(prev.left - next.left) < FALLBACK_RECT_MIN_CHANGE_PX && - Math.abs(prev.width - next.width) < FALLBACK_RECT_MIN_CHANGE_PX && - Math.abs(prev.height - next.height) < FALLBACK_RECT_MIN_CHANGE_PX - ? prev - : next - ) - } - - updateRect() - const body = findBody() - const parent = overlayRef.current?.parentElement - const resizeObserver = new ResizeObserver(updateRect) - if (body) { - resizeObserver.observe(body) - } - if (parent) { - resizeObserver.observe(parent) - } - window.addEventListener('resize', updateRect) - return () => { - resizeObserver.disconnect() - window.removeEventListener('resize', updateRect) - } - }, [anchorName, groupId, isVisible]) + return subscribeTabGroupBodyRect(groupId, container, setMeasuredFallbackRect) + }, [groupId, measuresFallbackRect]) useLayoutEffect(() => { if (!fitTerminal || !isVisible || !anchorName) { diff --git a/src/renderer/src/components/tab-group/TabGroupPanel.tsx b/src/renderer/src/components/tab-group/TabGroupPanel.tsx index 6e48ddd0ec0..f6ad4b04d89 100644 --- a/src/renderer/src/components/tab-group/TabGroupPanel.tsx +++ b/src/renderer/src/components/tab-group/TabGroupPanel.tsx @@ -1,4 +1,4 @@ -import { Suspense, useMemo } from 'react' +import { Suspense, useCallback, useMemo } from 'react' import { lazyWithRetry as lazy } from '@/lib/lazy-with-retry' import { useDroppable } from '@dnd-kit/core' import { Ellipsis, X } from 'lucide-react' @@ -18,6 +18,7 @@ import { closeTerminalTab } from '../terminal/terminal-tab-actions' import { resolveGroupTabFromVisibleId } from './tab-group-visible-id' import { getTabPaneBodyDroppableId, type HoveredTabInsertion } from './useTabDragSplit' import { tabGroupBodyAnchorName } from './tab-group-body-anchor' +import { registerTabGroupBody } from './tab-group-body-geometry' import { translate } from '@/i18n/i18n' import type { TabGroup } from '../../../../shared/tab-types' import type { ClientHostedBrowserRow } from '../../../../shared/client-hosted-browser-rows' @@ -93,6 +94,20 @@ export default function TabGroupPanel({ }, disabled: !isTabDragActive }) + const setBodyRef = useCallback( + (node: HTMLDivElement | null) => { + setBodyDropRef(node) + if (!node) { + return undefined + } + const unregisterBody = registerTabGroupBody(groupId, node) + return () => { + unregisterBody() + setBodyDropRef(null) + } + }, + [groupId, setBodyDropRef] + ) // Why: per-group anchor-name lets the worktree-level overlay position panes via CSS anchor positioning, so moving a tab between groups re-targets the anchor instead of remounting xterm (loses alt-screen TUI state) or reloading ``. const bodyAnchorName = tabGroupBodyAnchorName(groupId) // Why: memoize so a fresh style object each render doesn't break downstream memoization keyed on referential equality. @@ -334,7 +349,7 @@ export default function TabGroupPanel({
void + observed: Set + disconnected: boolean +} + +let observers: FakeObserver[] +let cleanups: (() => void)[] + +function element(rect: DOMRect): HTMLDivElement { + const node = document.createElement('div') + node.getBoundingClientRect = () => rect + return node +} + +function track(cleanup: () => void): () => void { + cleanups.push(cleanup) + return cleanup +} + +function resizeListenerDelta( + add: ReturnType, + remove: ReturnType +) { + const count = (spy: ReturnType): number => + spy.mock.calls.filter(([type]) => type === 'resize').length + return count(add) - count(remove) +} + +beforeEach(() => { + observers = [] + cleanups = [] + vi.stubGlobal( + 'ResizeObserver', + class { + private readonly record: FakeObserver + constructor(callback: () => void) { + this.record = { callback, observed: new Set(), disconnected: false } + observers.push(this.record) + } + observe(target: Element): void { + this.record.observed.add(target) + } + unobserve(target: Element): void { + this.record.observed.delete(target) + } + disconnect(): void { + this.record.disconnected = true + this.record.observed.clear() + } + } + ) +}) + +afterEach(() => { + cleanups.forEach((cleanup) => cleanup()) + vi.restoreAllMocks() + vi.unstubAllGlobals() +}) + +describe('subscribeTabGroupBodyRect', () => { + it('shares one observer per group and one window listener across groups', () => { + const add = vi.spyOn(window, 'addEventListener') + const remove = vi.spyOn(window, 'removeEventListener') + const container = element(new DOMRect(0, 10, 800, 600)) + track(registerTabGroupBody('a', element(new DOMRect(0, 42, 400, 568)))) + track(registerTabGroupBody('b', element(new DOMRect(400, 42, 400, 568)))) + + const first = vi.fn() + const second = vi.fn() + const other = vi.fn() + const unsubscribeFirst = subscribeTabGroupBodyRect('a', container, first) + const unsubscribeSecond = subscribeTabGroupBodyRect('a', container, second) + const unsubscribeOther = subscribeTabGroupBodyRect('b', container, other) + + expect(observers).toHaveLength(2) + expect(resizeListenerDelta(add, remove)).toBe(1) + expect(first).toHaveBeenLastCalledWith({ top: 32, left: 0, width: 400, height: 568 }) + expect(second.mock.lastCall?.[0]).toBe(first.mock.lastCall?.[0]) + expect(other).toHaveBeenLastCalledWith({ top: 32, left: 400, width: 400, height: 568 }) + + unsubscribeFirst() + expect(observers[0].disconnected).toBe(false) + unsubscribeSecond() + expect(observers[0].disconnected).toBe(true) + expect(resizeListenerDelta(add, remove)).toBe(1) + unsubscribeOther() + expect(observers[1].disconnected).toBe(true) + expect(resizeListenerDelta(add, remove)).toBe(0) + }) + + it('notifies only on changes larger than a pixel', () => { + const container = element(new DOMRect(0, 0, 800, 600)) + let bodyRect = new DOMRect(0, 32, 400, 568) + const body = element(bodyRect) + body.getBoundingClientRect = () => bodyRect + track(registerTabGroupBody('a', body)) + const listener = vi.fn() + track(subscribeTabGroupBodyRect('a', container, listener)) + listener.mockClear() + + bodyRect = new DOMRect(0, 32.5, 400.5, 568) + window.dispatchEvent(new Event('resize')) + expect(listener).not.toHaveBeenCalled() + + bodyRect = new DOMRect(0, 32, 380, 568) + observers[0].callback() + expect(listener).toHaveBeenCalledTimes(1) + expect(listener).toHaveBeenLastCalledWith({ top: 32, left: 0, width: 380, height: 568 }) + }) + + it('follows the group body as it mounts, is replaced, and unmounts', () => { + const container = element(new DOMRect(0, 0, 800, 600)) + const listener = vi.fn() + track(subscribeTabGroupBodyRect('a', container, listener)) + expect(listener).toHaveBeenLastCalledWith(null) + + const firstBody = element(new DOMRect(0, 32, 400, 568)) + const unregisterFirst = registerTabGroupBody('a', firstBody) + expect(observers[0].observed.has(firstBody)).toBe(true) + expect(listener).toHaveBeenLastCalledWith({ top: 32, left: 0, width: 400, height: 568 }) + + const secondBody = element(new DOMRect(0, 32, 600, 568)) + const unregisterSecond = track(registerTabGroupBody('a', secondBody)) + expect(observers[0].observed.has(firstBody)).toBe(false) + expect(observers[0].observed.has(secondBody)).toBe(true) + expect(listener).toHaveBeenLastCalledWith({ top: 32, left: 0, width: 600, height: 568 }) + + // A late unregister from the replaced panel must not drop the live body. + unregisterFirst() + expect(observers[0].observed.has(secondBody)).toBe(true) + + unregisterSecond() + expect(observers[0].observed.has(secondBody)).toBe(false) + expect(listener).toHaveBeenLastCalledWith(null) + }) +}) diff --git a/src/renderer/src/components/tab-group/tab-group-body-geometry.ts b/src/renderer/src/components/tab-group/tab-group-body-geometry.ts new file mode 100644 index 00000000000..28712fb0716 --- /dev/null +++ b/src/renderer/src/components/tab-group/tab-group-body-geometry.ts @@ -0,0 +1,171 @@ +// Why: every retained pane in a group sits over the same tab-group body, so the +// non-anchor fallback (web client) measures that body once per group instead of +// once per pane, and only while some pane in the group can display. + +export type TabGroupBodyRect = { + top: number + left: number + width: number + height: number +} + +type TabGroupBodyRectListener = (rect: TabGroupBodyRect | null) => void + +type GroupBodyGeometrySource = { + groupId: string + // The positioned element the pane hosts are laid out in; rects are relative to it. + container: HTMLElement + listeners: Set + observer: ResizeObserver + observedBody: HTMLElement | null + rect: TabGroupBodyRect | null +} + +const RECT_MIN_CHANGE_PX = 1 + +const bodiesByGroupId = new Map() +const sourcesByGroupId = new Map>() + +function isSameRect(prev: TabGroupBodyRect | null, next: TabGroupBodyRect | null): boolean { + if (!prev || !next) { + return prev === next + } + // Why: ResizeObserver and xterm fit can otherwise amplify sub-pixel jitter forever. + return ( + Math.abs(prev.top - next.top) < RECT_MIN_CHANGE_PX && + Math.abs(prev.left - next.left) < RECT_MIN_CHANGE_PX && + Math.abs(prev.width - next.width) < RECT_MIN_CHANGE_PX && + Math.abs(prev.height - next.height) < RECT_MIN_CHANGE_PX + ) +} + +function measureSource(source: GroupBodyGeometrySource): void { + const body = source.observedBody + let next: TabGroupBodyRect | null = null + if (body) { + const containerRect = source.container.getBoundingClientRect() + const bodyRect = body.getBoundingClientRect() + next = { + top: bodyRect.top - containerRect.top, + left: bodyRect.left - containerRect.left, + width: bodyRect.width, + height: bodyRect.height + } + } + if (isSameRect(source.rect, next)) { + return + } + source.rect = next + for (const listener of source.listeners) { + listener(next) + } +} + +function measureAllSources(): void { + for (const sources of sourcesByGroupId.values()) { + for (const source of sources) { + measureSource(source) + } + } +} + +function syncObservedBody(source: GroupBodyGeometrySource): void { + const body = bodiesByGroupId.get(source.groupId) ?? null + if (body === source.observedBody) { + return + } + if (source.observedBody) { + source.observer.unobserve(source.observedBody) + } + if (body) { + source.observer.observe(body) + } + source.observedBody = body +} + +function onGroupBodyChanged(groupId: string): void { + for (const source of sourcesByGroupId.get(groupId) ?? []) { + syncObservedBody(source) + measureSource(source) + } +} + +/** Registers the element that is the body of `groupId`; returns the unregister function. */ +export function registerTabGroupBody(groupId: string, body: HTMLElement): () => void { + bodiesByGroupId.set(groupId, body) + onGroupBodyChanged(groupId) + return () => { + // A remounted panel may already have registered its replacement body. + if (bodiesByGroupId.get(groupId) !== body) { + return + } + bodiesByGroupId.delete(groupId) + onGroupBodyChanged(groupId) + } +} + +function acquireSource(groupId: string, container: HTMLElement): GroupBodyGeometrySource { + let sources = sourcesByGroupId.get(groupId) + for (const existing of sources ?? []) { + if (existing.container === container) { + return existing + } + } + const source: GroupBodyGeometrySource = { + groupId, + container, + listeners: new Set(), + observer: new ResizeObserver(() => measureSource(source)), + observedBody: null, + rect: null + } + source.observer.observe(container) + syncObservedBody(source) + if (sourcesByGroupId.size === 0) { + window.addEventListener('resize', measureAllSources) + } + if (!sources) { + sources = new Set() + sourcesByGroupId.set(groupId, sources) + } + sources.add(source) + return source +} + +function releaseSource(source: GroupBodyGeometrySource): void { + source.observer.disconnect() + const sources = sourcesByGroupId.get(source.groupId) + sources?.delete(source) + if (sources?.size === 0) { + sourcesByGroupId.delete(source.groupId) + } + if (sourcesByGroupId.size === 0) { + window.removeEventListener('resize', measureAllSources) + } +} + +/** + * Subscribes to the rect of `groupId`'s body relative to `container`. The + * listener is called synchronously with the current rect, then on each change. + */ +export function subscribeTabGroupBodyRect( + groupId: string, + container: HTMLElement, + listener: TabGroupBodyRectListener +): () => void { + const source = acquireSource(groupId, container) + // Why: observers miss moves without a resize, so a pane that becomes visible re-reads once. + measureSource(source) + listener(source.rect) + // A per-subscription entry keeps unsubscribe idempotent even if one listener subscribes twice. + const entry: TabGroupBodyRectListener = (rect) => listener(rect) + source.listeners.add(entry) + return () => { + if (!source.listeners.delete(entry)) { + return + } + if (source.listeners.size === 0) { + releaseSource(source) + } + } +} diff --git a/src/renderer/src/components/task-page-source-family.test-support.ts b/src/renderer/src/components/task-page-source-family.test-support.ts index 2253f11295e..14dd9a76d2e 100644 --- a/src/renderer/src/components/task-page-source-family.test-support.ts +++ b/src/renderer/src/components/task-page-source-family.test-support.ts @@ -1,25 +1,8 @@ import { readFileSync, readdirSync } from 'node:fs' import { join } from 'node:path' -import { - type HookContractFact, - readFlattenedHookFacts, - readHookDefinitions -} from './refactor-hook-contract.test-support' - const TASK_PAGE_FLAT_SOURCE_PATTERN = /^(?:use-task-page-.*\.ts|task-page-.*\.tsx?)$/ const TASK_PAGE_DIRECTORY = 'task-page' -const TASK_PAGE_EXTRACTED_LOWERCASE_FILES = new Set([ - 'task-page-draft-storage.tsx', - 'task-page-github-landing-refresh-run.tsx', - 'task-page-github-quiet-refresh-run.tsx', - 'task-page-github-review-model.tsx', - 'task-page-github-reviewer-actions.ts', - 'task-page-linear-issue-model.tsx', - 'task-page-linear-jira-list-model.tsx', - 'task-page-source-context.tsx' -]) - function isSourceFile(name: string): boolean { return !name.includes('.test.') && !name.includes('.test-support.') } @@ -45,13 +28,6 @@ export const TASK_PAGE_SOURCE_FILES = [ ...readTaskPageDirectory(TASK_PAGE_DIRECTORY) ].sort() -export const TASK_PAGE_REFACTOR_SOURCE_FILES = TASK_PAGE_SOURCE_FILES.filter( - (name) => - (name.startsWith(`${TASK_PAGE_DIRECTORY}/`) && name.endsWith('.tsx')) || - /^use-task-page-.*\.ts$/.test(name) || - TASK_PAGE_EXTRACTED_LOWERCASE_FILES.has(name) -) - export function readTaskPageSource(relativePath: string): string { return readFileSync(join(__dirname, relativePath), 'utf8') } @@ -61,22 +37,3 @@ export function readTaskPageSourceFamily(): string { (relativePath) => `// TaskPage source: ${relativePath}\n${readTaskPageSource(relativePath)}` ).join('\n') } - -export function readTaskPageRefactorSourceFamily(): string { - return TASK_PAGE_REFACTOR_SOURCE_FILES.map(readTaskPageSource).join('\n') -} - -export function readFlattenedTaskPageHookFacts(): HookContractFact[] { - const definitions = readHookDefinitions( - TASK_PAGE_REFACTOR_SOURCE_FILES.map((relativePath) => ({ - relativePath, - source: readTaskPageSource(relativePath) - })), - (name) => name === 'TaskPage' || name.startsWith('useTaskPage') - ) - return readFlattenedHookFacts(definitions, 'TaskPage') -} - -export function readFlattenedTaskPageHookOrder(): string[] { - return readFlattenedTaskPageHookFacts().map(({ name }) => name) -} diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx index dbdca355779..cf9cabd9c57 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx @@ -23,6 +23,7 @@ vi.mock('../../store', () => ({ })) import { TerminalOverlaySlot } from './TerminalOverlaySlot' +import { registerTabGroupBody } from '../tab-group/tab-group-body-geometry' const GROUP_ID = 'group-react185' const TAB_ID = 'tab-react185' @@ -52,6 +53,7 @@ let capturedResizeCallback: (() => void) | null = null let container: HTMLDivElement let bodyEl: HTMLDivElement let bodyRect: DOMRect +let unregisterBody: () => void let root: Root class CapturingResizeObserver { @@ -103,12 +105,14 @@ beforeEach(() => { bodyRect = createRect({ top: 32, height: 568 }) bodyEl.getBoundingClientRect = () => bodyRect document.body.appendChild(bodyEl) + unregisterBody = registerTabGroupBody(GROUP_ID, bodyEl) }) afterEach(() => { act(() => { root?.unmount() }) + unregisterBody() container?.remove() bodyEl?.remove() vi.unstubAllGlobals() diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx index 0b5da641c95..7eda851e7e9 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx @@ -92,14 +92,14 @@ export function TerminalPaneSurface({ searchStateRef, searchInputRef, sessionRestoredBannerPaneIds, - sessionStateSaveFailureOpen, + sessionStateSaveFailureMessage, setAgentSessionContinuation, setAgentSessionFork, setContainerRef, setQuickCommandEditorOpen, setRenameValue, setSearchOpen, - setSessionStateSaveFailureOpen, + setSessionStateSaveFailureMessage, showSplitButton, showSshReconnectOverlay, splitTerminalPaneFromHeader, @@ -201,8 +201,9 @@ export function TerminalPaneSurface({ {isActive && ( setSessionStateSaveFailureOpen(false)} + open={sessionStateSaveFailureMessage !== null} + failureMessage={sessionStateSaveFailureMessage ?? ''} + onDismiss={() => setSessionStateSaveFailureMessage(null)} onOpenSpaceAnalyzer={openDiskSpaceAnalyzer} /> )} diff --git a/src/renderer/src/components/terminal-pane/TerminalSessionStateSaveFailureDialog.tsx b/src/renderer/src/components/terminal-pane/TerminalSessionStateSaveFailureDialog.tsx index 6ab425685a3..889fa69d7ef 100644 --- a/src/renderer/src/components/terminal-pane/TerminalSessionStateSaveFailureDialog.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalSessionStateSaveFailureDialog.tsx @@ -1,4 +1,4 @@ -import { HardDrive } from 'lucide-react' +import { AlertCircle } from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, @@ -9,16 +9,20 @@ import { DialogTitle } from '@/components/ui/dialog' import { translate } from '@/i18n/i18n' +import { isTerminalSessionStorageCapacityFailure } from '../../../../shared/terminal-session-state-save-failure' export function TerminalSessionStateSaveFailureDialog({ open, + failureMessage, onDismiss, onOpenSpaceAnalyzer }: { open: boolean + failureMessage: string onDismiss: () => void onOpenSpaceAnalyzer: () => void }): React.JSX.Element { + const capacityFailure = isTerminalSessionStorageCapacityFailure(failureMessage) return (
- +
- {translate( - 'auto.components.terminal.pane.TerminalSessionStateSaveFailureDialog.678c780a2c', - 'Disk space is unavailable' - )} + {translate('terminal.sessionSaveFailure.title', 'Could not save terminal session')}
- {translate( - 'auto.components.terminal.pane.TerminalSessionStateSaveFailureDialog.e2fcf07c0d', - 'Orca could not save this terminal session because local storage is full or not writable. Open the disk space analyzer to find workspace storage you can clean up.' - )} + {capacityFailure + ? translate( + 'terminal.sessionSaveFailure.capacity', + 'The device saving this terminal reported full storage or an exceeded storage quota. Free space on that device, then try again.' + ) + : translate( + 'terminal.sessionSaveFailure.unknown', + 'Orca could not save the terminal state. Try again. If this continues, share the Orca logs with support so we can identify the cause.' + )} -
- {translate( - 'auto.components.terminal.pane.TerminalSessionStateSaveFailureDialog.38c282a2c4', - 'The analyzer opens directly from here. You can also open it later from the lower-left toolbox menu by choosing Space Analyzer.' - )} -
+ {capacityFailure && ( +
+ {translate( + 'auto.components.terminal.pane.TerminalSessionStateSaveFailureDialog.38c282a2c4', + 'The analyzer opens directly from here. You can also open it later from the lower-left toolbox menu by choosing Space Analyzer.' + )} +
+ )} - + {capacityFailure && ( + + )}
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 7fd0de6952e..dce25fd547c 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport-types.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport-types.ts @@ -212,6 +212,8 @@ export type PtyTransport = { getRecoveryState?: () => PtyTransportRecoveryState /** Starts a fresh connection epoch while preserving the authoritative remote PTY identity. */ retryRecovery?: () => boolean + /** Lets a wrapper retain input when recovery re-enters connect internally. */ + setConnectForRecovery?: (connect: PtyTransport['connect']) => void /** The user dismissed the error surface; the next occurrence of the same message must surface again. */ notifyErrorSurfaceDismissed?: () => void getPtyId: () => string | null diff --git a/src/renderer/src/components/terminal-pane/remote-reattach-input-buffer.test.ts b/src/renderer/src/components/terminal-pane/remote-reattach-input-buffer.test.ts new file mode 100644 index 00000000000..9b6a731b158 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/remote-reattach-input-buffer.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it, vi } from 'vitest' +import { createDeferred, flushAsyncTicks } from './pty-connection-test-async' +import type { PtyConnectResult, PtyTransport } from './pty-transport-types' +import { withRemoteReattachInputBuffer } from './remote-reattach-input-buffer' + +const ORIGINAL_ID = 'remote:environment@@term_original' + +function createHarness() { + const connected = createDeferred() + let ptyId: string | null = null + const writes: string[] = [] + const delegate: PtyTransport = { + connect: vi.fn(() => connected.promise), + attach: vi.fn(), + disconnect: vi.fn(), + detach: vi.fn(), + destroy: vi.fn(), + isConnected: () => ptyId !== null, + getPtyId: () => ptyId, + resize: () => true, + sendInput: vi.fn((data) => { + if (!ptyId) { + return false + } + writes.push(data) + return true + }), + sendInputImmediate: vi.fn(() => false), + sendInputAccepted: vi.fn(async (data) => { + if (!ptyId) { + return false + } + writes.push(data) + return true + }) + } + return { + delegate, + connected, + writes, + transport: withRemoteReattachInputBuffer(delegate), + finish(id: string | null = ORIGINAL_ID) { + ptyId = id + connected.resolve(id ? { id, isReattach: true } : undefined) + } + } +} + +describe('remote reattach type-ahead', () => { + it('preserves typing from the restored screen before connect starts', async () => { + const harness = createHarness() + expect(harness.transport.sendInput('before', 'driving')).toBe(true) + const accepted = harness.transport.sendInputAccepted?.('\x03', 'driving') + const connecting = harness.transport.connect({ url: '', sessionId: ORIGINAL_ID, callbacks: {} }) + expect(harness.transport.sendInput('after\r', 'driving')).toBe(true) + harness.finish() + await connecting + await expect(accepted).resolves.toBe(true) + expect(harness.writes).toEqual(['before', '\x03', 'after\r']) + }) + + it('discards pre-entry typing when the first connection creates a fresh terminal', async () => { + const harness = createHarness() + const accepted = harness.transport.sendInputAccepted?.('old tail\r', 'driving') + const connecting = harness.transport.connect({ url: '', callbacks: {} }) + await expect(accepted).resolves.toBe(false) + harness.finish('remote:environment@@fresh') + await connecting + expect(harness.writes).toEqual([]) + }) + + it('delivers typing and an acknowledged interrupt once the original endpoint reattaches', async () => { + const harness = createHarness() + const connecting = harness.transport.connect({ url: '', sessionId: ORIGINAL_ID, callbacks: {} }) + expect(harness.transport.sendInput('partial', 'driving')).toBe(true) + const accepted = harness.transport.sendInputAccepted?.('\x03', 'driving') + expect(harness.transport.sendInput('next\r', 'driving')).toBe(true) + expect(harness.writes).toEqual([]) + harness.finish() + await connecting + await expect(accepted).resolves.toBe(true) + expect(harness.writes).toEqual(['partial', '\x03', 'next\r']) + expect(harness.transport.sendInput('later', 'driving')).toBe(true) + expect(harness.writes.at(-1)).toBe('later') + }) + + it.each(['remote:environment@@term_replacement', null])( + 'never forwards retained input when the original endpoint is unavailable: %s', + async (replacementId) => { + const harness = createHarness() + const connecting = harness.transport.connect({ + url: '', + sessionId: ORIGINAL_ID, + callbacks: {} + }) + const accepted = harness.transport.sendInputAccepted?.('dangerous tail\r', 'driving') + harness.finish(replacementId) + await connecting + await expect(accepted).resolves.toBe(false) + expect(harness.writes).toEqual([]) + } + ) + + it.each(['disconnect', 'detach', 'destroy'] as const)( + 'drops input and settles accepted writes on %s before a late reattach', + async (action) => { + const harness = createHarness() + const connecting = harness.transport.connect({ + url: '', + sessionId: ORIGINAL_ID, + callbacks: {} + }) + const accepted = harness.transport.sendInputAccepted?.('stale\r', 'driving') + harness.transport[action]?.() + await expect(accepted).resolves.toBe(false) + harness.finish() + await connecting + expect(harness.writes).toEqual([]) + } + ) + + it('keeps newly typed bytes behind a pending acknowledged write', async () => { + const harness = createHarness() + const acknowledgement = createDeferred() + harness.delegate.sendInputAccepted = () => acknowledgement.promise + const transport = withRemoteReattachInputBuffer(harness.delegate) + const connecting = transport.connect({ url: '', sessionId: ORIGINAL_ID, callbacks: {} }) + const accepted = transport.sendInputAccepted?.('\x03', 'driving') + harness.finish() + await flushAsyncTicks() + expect(transport.sendInput('after interrupt\r', 'driving')).toBe(true) + expect(harness.writes).toEqual([]) + acknowledgement.resolve(true) + await connecting + await expect(accepted).resolves.toBe(true) + expect(harness.writes).toEqual(['after interrupt\r']) + }) + + it('rejects pending writes when connection fails and leaves query replies unbuffered', async () => { + const harness = createHarness() + const connecting = harness.transport.connect({ url: '', sessionId: ORIGINAL_ID, callbacks: {} }) + const accepted = harness.transport.sendInputAccepted?.('typed', 'driving') + expect(harness.transport.sendInput('\x1b[I', 'query-reply')).toBe(false) + expect(harness.transport.sendInputImmediate('\x1b[1;1R')).toBe(false) + harness.connected.reject(new Error('offline')) + await expect(connecting).rejects.toThrow('offline') + await expect(accepted).resolves.toBe(false) + expect(harness.writes).toEqual([]) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/remote-reattach-input-buffer.ts b/src/renderer/src/components/terminal-pane/remote-reattach-input-buffer.ts new file mode 100644 index 00000000000..6e5969a74c8 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/remote-reattach-input-buffer.ts @@ -0,0 +1,90 @@ +import { parseRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' +import { createPtyPreconnectInputBuffer } from './pty-preconnect-input-buffer' +import type { PtyTransport } from './pty-transport-types' + +/** Keeps type-ahead on a restored screen bound to its original remote terminal. */ +export function withRemoteReattachInputBuffer(transport: PtyTransport): PtyTransport { + const sendAccepted = transport.sendInputAccepted?.bind(transport) + let pending: ReturnType | null = + createPtyPreconnectInputBuffer() + let entered = false + const clear = (): void => { + pending?.clear() + pending = null + } + const wrapped: PtyTransport = { + ...transport, + async connect(options) { + const expectedId = options.sessionId + const initialBuffer = entered ? null : pending + entered = true + const buffer = + expectedId && parseRemoteRuntimePtyId(expectedId) + ? (initialBuffer ?? createPtyPreconnectInputBuffer()) + : null + // Restored pixels can accept typing before the first connect call begins. + if (pending !== buffer) { + clear() + } + pending = buffer + try { + const result = await transport.connect(options) + if (buffer) { + await buffer.flush({ + // A replacement endpoint must never receive the old terminal's unfinished command. + isCurrent: () => pending === buffer && transport.getPtyId() === expectedId, + sendInput: (data, kind) => transport.sendInput(data, kind), + sendInputImmediate: (data) => transport.sendInputImmediate(data), + ...(sendAccepted ? { sendInputAccepted: sendAccepted } : {}) + }) + } + return result + } finally { + buffer?.clear() + if (pending === buffer) { + pending = null + } + } + }, + sendInput(data, kind) { + return kind !== 'query-reply' && pending?.isBuffering() + ? pending.enqueue(data, 'ordinary', kind) + : transport.sendInput(data, kind) + }, + // Emulator replies stay on the immediate path; replay must not retain them as user input. + ...(sendAccepted + ? { + sendInputAccepted: (data, kind) => + kind !== 'query-reply' && pending?.isBuffering() + ? pending.enqueueAccepted(data, kind) + : sendAccepted(data, kind) + } + : {}), + attach(options) { + entered = true + clear() + transport.attach(options) + }, + disconnect() { + entered = true + clear() + transport.disconnect() + }, + ...(transport.detach + ? { + detach: (options) => { + entered = true + clear() + transport.detach?.(options) + } + } + : {}), + destroy(options) { + entered = true + clear() + return transport.destroy?.(options) + } + } + transport.setConnectForRecovery?.((options) => wrapped.connect(options)) + return wrapped +} diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-reattach-type-ahead.test.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-reattach-type-ahead.test.ts new file mode 100644 index 00000000000..94fd6f82853 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-reattach-type-ahead.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { + TerminalStreamOpcode, + decodeTerminalStreamText +} from '../../../../shared/terminal-stream-protocol' +import { createDeferred } from './pty-connection-test-async' +import { + createRemoteRuntimeTransportMocks, + type MultiplexSubscriptionCallbacks +} from './remote-runtime-pty-transport-test-harness' + +let callbacks: MultiplexSubscriptionCallbacks = null +let resolvedHandle = 'terminal-1' +const { runtimeCall, latestFrameForOpcode, resetRemoteRuntimeTransport } = + createRemoteRuntimeTransportMocks({ + getCallbacks: () => callbacks, + setCallbacks: (value) => { + callbacks = value + }, + getResolvedPaneHandle: () => resolvedHandle, + setResolvedPaneHandle: (value) => { + resolvedHandle = value + } + }) + +beforeEach(resetRemoteRuntimeTransport) + +it('sends typing from a restored screen after the host finishes resolving its original pane', async () => { + const resolution = createDeferred() + const call = runtimeCall.getMockImplementation() + runtimeCall.mockImplementation(async (request: { method: string }) => { + if (request.method === 'terminal.resolvePane') { + await resolution.promise + } + return call?.(request) + }) + const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport') + const transport = createRemoteRuntimePtyTransport('env-1', { + worktreeId: 'wt-1', + tabId: 'web-terminal-host-tab-1', + leafId: 'pane:1' + }) + try { + expect(transport.sendInput('probe-', 'driving')).toBe(true) + const connecting = transport.connect({ + url: '', + sessionId: 'remote:env-1@@terminal-1', + callbacks: {} + }) + await vi.waitFor(() => { + expect(runtimeCall).toHaveBeenCalledWith( + expect.objectContaining({ method: 'terminal.resolvePane' }) + ) + }) + expect(transport.getPtyId()).toBeNull() + expect(transport.sendInput('cold-parked\r', 'driving')).toBe(true) + expect(latestFrameForOpcode(TerminalStreamOpcode.Input)).toBeUndefined() + resolution.resolve() + await connecting + await vi.waitFor(() => { + const frame = latestFrameForOpcode(TerminalStreamOpcode.Input) + expect(frame && decodeTerminalStreamText(frame.payload)).toBe('probe-cold-parked\r') + }) + } finally { + resolution.resolve() + transport.destroy?.() + } +}) diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts index a8e8a727ad8..cb78db75e9e 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts @@ -1,4 +1,5 @@ import { createAgentSessionKeyboardOptions } from '@/runtime/agent-session-keyboard-capability' +import { withRemoteReattachInputBuffer } from './remote-reattach-input-buffer' /* eslint-disable max-lines -- Why: remote PTY transport keeps lifecycle, JSON fallback, and binary stream wiring together so reconnect/destroy ordering stays testable as one behavior surface. */ import type { RuntimeRpcResponse } from '../../../../shared/runtime-rpc-envelope' import { @@ -885,7 +886,7 @@ export function createRemoteRuntimePtyTransport( return true } if (lastConnectOptions) { - void transport.connect(lastConnectOptions) + void connectForRecovery(lastConnectOptions) return true } return false @@ -2173,6 +2174,7 @@ export function createRemoteRuntimePtyTransport( flushPendingClaimInput(nextStream) } + let connectForRecovery: PtyTransport['connect'] = (options) => transport.connect(options) const transport: PtyTransport = { async connect(options) { cancelTerminalCreateRetryWait() @@ -2715,7 +2717,7 @@ export function createRemoteRuntimePtyTransport( recovery.currentPhase === 'disconnected' ) { recovery.begin() - void transport.connect(lastConnectOptions) + void connectForRecovery(lastConnectOptions) return true } // Why: online/resume fires a parked retry; the button must not be weaker than an event (#12684). @@ -2785,6 +2787,9 @@ export function createRemoteRuntimePtyTransport( return stream.serializeBufferOutcome(opts) }, + setConnectForRecovery(connect) { + connectForRecovery = connect + }, destroy() { destroyed = true setAttachmentUnavailable() @@ -2800,5 +2805,5 @@ export function createRemoteRuntimePtyTransport( viewportBatcher.clear() } } - return transport + return withRemoteReattachInputBuffer(transport) } diff --git a/src/renderer/src/components/terminal-pane/terminal-command-lifecycle.ts b/src/renderer/src/components/terminal-pane/terminal-command-lifecycle.ts index 61ad0891fa6..5d60514842e 100644 --- a/src/renderer/src/components/terminal-pane/terminal-command-lifecycle.ts +++ b/src/renderer/src/components/terminal-pane/terminal-command-lifecycle.ts @@ -15,9 +15,9 @@ export function createTerminalCommandLifecycle(options: TerminalCommandLifecycle // Why: the byte parsing lives in shared so main's side-effect tracker emits // identical command-finished facts for local/SSH PTYs; this renderer wrapper // remains the byte path for remote-runtime PTYs and the kill-switch-off mode. - const scanner = createOsc133CommandFinishedScanner( - options.onCommandFinished, - options.onCommandStarted + // Why the wrapper: the scanner passes a chunk offset, which a callback with its own optional parameter must not receive. + const scanner = createOsc133CommandFinishedScanner(options.onCommandFinished, () => + options.onCommandStarted?.() ) const disposables: IDisposable[] = [] 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 deleted file mode 100644 index ded0bbf5c2a..00000000000 --- a/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { createHash } from 'node:crypto' -import { readFileSync, readdirSync } from 'node:fs' -import { join } from 'node:path' -// TypeScript 7 is a native CLI; AST tests still need the legacy JavaScript API. -import ts from 'typescript-api' -import { describe, expect, it } from 'vitest' - -const TERMINAL_PANE_HOOK_SOURCE_PATTERN = - /^(?:TerminalPane\.tsx|use-terminal-pane-(?:chat-state|close-actions|context-actions|controller|foundation|global-listeners|layout-bindings|layout-persistence|lifecycle-stage|mobile-actions|paste-listeners|process-exit-actions|projection|reconciliation|startup-actions|store-actions|store-bindings|title-effects|title-state)\.ts)$/ -// Rebased onto main after the workbench surface-per-workspace and deferred -// split-cwd changes; the pane session-ID projection added one render hook (230 hooks). -// Then 27 stable-action `useAppStore` subscriptions folded into four -// `useTerminalPaneStoreActions()` calls, each one `useMemo` (204 hooks, 8 useMemo). -// Restoring the terminal/chat switcher added four `useCallback`s -- three in -// chat-state (can-toggle, toggle-for-leaf, toggle-active) and the context-menu -// toggle in projection (208 hooks, still 8 useMemo). -// Then chat-state's orchestration dispatch-status subscription went with the -// 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). -// Then the dead adopted-structured-session portal went with its local target `useMemo` -// in projection (211 hooks, 7 useMemo). -// Then chat ownership through toggles and restore (#23049) added a `useRef`, a `useLayoutEffect` -// and a `useEffect` across chat-state, layout-persistence and title-effects (214 hooks, still 7 useMemo). -const PRE_REFACTOR_HOOK_ORDER_SHA256 = - '3736b71c612bb28b9b5298c7704ee1b32c7afc0254e75d0072ff4df870ec3be9' - -const sourceFiles = readdirSync(__dirname) - .filter((name) => TERMINAL_PANE_HOOK_SOURCE_PATTERN.test(name)) - .sort() - -function readFunctionDefinitions(): Map { - const definitions = new Map() - for (const relativePath of sourceFiles) { - const filePath = join(__dirname, relativePath) - const sourceFile = ts.createSourceFile( - filePath, - readFileSync(filePath, 'utf8'), - ts.ScriptTarget.Latest, - true, - relativePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS - ) - const visit = (node: ts.Node): void => { - if ( - ts.isFunctionDeclaration(node) && - node.name && - (node.name.text === 'TerminalPane' || node.name.text.startsWith('useTerminalPane')) - ) { - definitions.set(node.name.text, node) - } - ts.forEachChild(node, visit) - } - visit(sourceFile) - } - return definitions -} - -function readFlattenedHookOrder(): string[] { - const definitions = readFunctionDefinitions() - const flatten = (name: string, active: ReadonlySet): string[] => { - const definition = definitions.get(name) - if (!definition?.body) { - throw new Error(`Missing terminal pane hook stage: ${name}`) - } - if (active.has(name)) { - throw new Error(`Recursive terminal pane hook stage: ${name}`) - } - const nextActive = new Set([...active, name]) - const hooks: string[] = [] - const visit = (node: ts.Node): void => { - if ( - ts.isCallExpression(node) && - ts.isIdentifier(node.expression) && - /^use[A-Z]/.test(node.expression.text) - ) { - const hookName = node.expression.text - hooks.push(...(definitions.has(hookName) ? flatten(hookName, nextActive) : [hookName])) - } - ts.forEachChild(node, visit) - } - visit(definition.body) - return hooks - } - - return flatten('TerminalPane', new Set()) -} - -describe('TerminalPane refactor hook parity', () => { - it('preserves the recursively flattened render hook order', () => { - const hooks = readFlattenedHookOrder() - expect(hooks).toHaveLength(214) - expect(hooks.filter((hook) => hook === 'useMemo')).toHaveLength(7) - expect(createHash('sha256').update(hooks.join('\n')).digest('hex')).toBe( - PRE_REFACTOR_HOOK_ORDER_SHA256 - ) - }) - - it('aggregates every extracted hook stage', () => { - expect(sourceFiles).toHaveLength(20) - expect(sourceFiles).toContain('TerminalPane.tsx') - expect(sourceFiles).toContain('use-terminal-pane-controller.ts') - }) - - it('defers quick-command host work until the context menu opens', () => { - const startupSource = readFileSync( - join(__dirname, 'use-terminal-pane-startup-actions.ts'), - 'utf8' - ) - const contextSource = readFileSync( - join(__dirname, 'use-terminal-pane-context-actions.ts'), - 'utf8' - ) - - expect(startupSource).toContain('const quickCommandRepoId =') - expect(startupSource).not.toContain('useTerminalQuickCommandHosts') - expect(contextSource).toContain('useTerminalQuickCommandHosts(worktreeId, contextMenu.open)') - expect(contextSource).toContain('terminalQuickCommandMatchesWorkspaceProject') - expect(contextSource).toContain('projectHostSetupProjection.setups') - expect(contextSource).toContain('quickCommandExecutionHostId') - }) -}) diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-quick-command-initialization.test.ts b/src/renderer/src/components/terminal-pane/terminal-pane-quick-command-initialization.test.ts new file mode 100644 index 00000000000..b64f7583160 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-pane-quick-command-initialization.test.ts @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +describe('TerminalPane quick-command initialization', () => { + it('defers quick-command host work until the context menu opens', () => { + const startupSource = readFileSync( + join(__dirname, 'use-terminal-pane-startup-actions.ts'), + 'utf8' + ) + const contextSource = readFileSync( + join(__dirname, 'use-terminal-pane-context-actions.ts'), + 'utf8' + ) + + expect(startupSource).toContain('const quickCommandRepoId =') + expect(startupSource).not.toContain('useTerminalQuickCommandHosts') + expect(contextSource).toContain('useTerminalQuickCommandHosts(worktreeId, contextMenu.open)') + expect(contextSource).toContain('terminalQuickCommandMatchesWorkspaceProject') + expect(contextSource).toContain('projectHostSetupProjection.setups') + expect(contextSource).toContain('quickCommandExecutionHostId') + }) +}) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts index 7a0da0ec79e..008925a0688 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts @@ -148,7 +148,9 @@ export function useTerminalPaneFoundation( const [ptyRecoveryStatesByPaneId, setPtyRecoveryStatesByPaneId] = useState< Record >({}) - const [sessionStateSaveFailureOpen, setSessionStateSaveFailureOpen] = useState(false) + const [sessionStateSaveFailureMessage, setSessionStateSaveFailureMessage] = useState< + string | null + >(null) const daemonActions = useDaemonActions() const { refreshMobileOverlays } = useMobileOverlayTicks({ managerRef, @@ -235,8 +237,8 @@ export function useTerminalPaneFoundation( setTerminalError, ptyRecoveryStatesByPaneId, setPtyRecoveryStatesByPaneId, - sessionStateSaveFailureOpen, - setSessionStateSaveFailureOpen, + sessionStateSaveFailureMessage, + setSessionStateSaveFailureMessage, daemonActions, refreshMobileOverlays } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts index 5f59aa73e25..deff696c93f 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts @@ -46,7 +46,7 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr setQuickCommandEditorHostId, setQuickCommandEditorOpen, setSessionRestoredBannerPaneIds, - setSessionStateSaveFailureOpen, + setSessionStateSaveFailureMessage, setShouldMeasureHiddenStartup, setTerminalError, setTerminalErrorsByPaneId, @@ -114,7 +114,7 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr ) const openDiskSpaceAnalyzer = useCallback(() => { - setSessionStateSaveFailureOpen(false) + setSessionStateSaveFailureMessage(null) openSpacePage() void refreshWorkspaceSpace().catch((error: unknown) => { console.warn('Failed to refresh Space Analyzer after terminal session save failure:', error) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts index 07db5bef38c..5c0522c97f1 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts @@ -17,7 +17,7 @@ export function useTerminalPaneTitleState(controller: TerminalPaneFoundation) { managerRef, paneTransportsRef, setPtyRecoveryStatesByPaneId, - setSessionStateSaveFailureOpen, + setSessionStateSaveFailureMessage, setTerminalError, setTerminalErrorsByPaneId, sshReconnectOwnsTerminalErrorsRef @@ -89,7 +89,7 @@ export function useTerminalPaneTitleState(controller: TerminalPaneFoundation) { if (isTerminalSessionStateSaveFailure(message)) { setTerminalError(null) setTerminalErrorsByPaneId({}) - setSessionStateSaveFailureOpen(true) + setSessionStateSaveFailureMessage(message) return } const visibleMessage = sshReconnectOwnsTerminalErrorsRef.current diff --git a/src/renderer/src/hooks/use-delayed-status.ts b/src/renderer/src/hooks/use-delayed-status.ts new file mode 100644 index 00000000000..c895107bb4a --- /dev/null +++ b/src/renderer/src/hooks/use-delayed-status.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' +import { createDelayedStatus, type ShownStatus } from '@/lib/delayed-status' + +/** + * Returns `value` only once it has lasted past `showDelayMs`, then holds it for a + * minimum time, so a short status never flashes. `key` is what the status belongs + * to (for example a session); a new key drops it at once. + */ +export function useDelayedStatus(key: string, value: A | null, showDelayMs: number): A | null { + const [shown, setShown] = useState | null>(null) + const [status] = useState(() => createDelayedStatus(setShown, { showDelayMs })) + useEffect(() => () => status.dispose(), [status]) + useEffect(() => { + status.update(key, value) + }, [status, key, value]) + return shown?.key === key ? shown.value : null +} diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 57703fb7d1f..90fc6192a11 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -99,11 +99,11 @@ "setupKindGit": "Git repo" }, "PullRequestPage": { - "3450247584": "Comment", "19f19560d5": "destructive", "1ff5d979df": "No one assigned", "2b4fdb880c": "Unmark viewed", "2e528e1c2d": "Viewed", + "3450247584": "Comment", "42c36d9166": "{{value0}} {{value1}} reaction{{value2}}", "4b960e5978": "Loading code context…", "50b8fb290f": "Mark viewed", @@ -245,9 +245,9 @@ "f0202f3a89": "Daily" }, "AutomationsPage": { - "2695883141": "delete", "0329f9bef1": "Close · Esc", "0ae52dd760": "hermes", + "2695883141": "delete", "3e42a5cc1b": "SSH connection failed.", "53f06f0ad5": "Retry source", "587a4b205c": "Next", @@ -297,13 +297,6 @@ "unknownManager": "This external automation is no longer listed for any host in view." } }, - "browser-pane": { - "markup": { - "errorAttach": "Could not attach the markup screenshot.", - "errorCapture": "Could not capture the page to draw on.", - "errorUnavailable": "Screenshot markup is not available on this page." - } - }, "browser": { "pane": { "BrowserPane": { @@ -340,6 +333,13 @@ } } }, + "browser-pane": { + "markup": { + "errorAttach": "Could not attach the markup screenshot.", + "errorCapture": "Could not capture the page to draw on.", + "errorUnavailable": "Screenshot markup is not available on this page." + } + }, "contextual": { "tours": { "ContextualTourControl": { @@ -595,9 +595,6 @@ } } }, - "linear-issue-attribute-filter-dropdowns": { - "optionsFromTeam": "Options from {{team}}" - }, "linear": { "api": { "key": { @@ -616,6 +613,9 @@ } } }, + "linear-issue-attribute-filter-dropdowns": { + "optionsFromTeam": "Options from {{team}}" + }, "mobile": { "MobileHero": { "79d2f480da": "Network interface to advertise", @@ -878,9 +878,9 @@ }, "empty": { "state": { - "3322603418": "Pull request status unavailable", "13e1c7d5ed": "No {{value0}} found", "2bdd7aaf2d": "GitHub status could not be refreshed. Existing cached data was preserved.", + "3322603418": "Pull request status unavailable", "3d4af82ff4": "Refreshing GitHub status for this branch", "41252bc53f": "Branch not published", "5b0cfae9a5": "Create a {{value0}} to start checks and review.", @@ -2005,6 +2005,15 @@ } } } + }, + "worktree": { + "card": { + "compact": { + "agents": { + "a128d7006b": "{{value0}} {{value1}} child {{value2}}" + } + } + } } }, "skills": { @@ -2471,6 +2480,10 @@ "TerminalRemoteRuntimeReconnectBanner": { "retryingBody": "Orca will retry for up to one minute. This terminal will resume if the connection returns." }, + "TerminalSessionStateSaveFailureDialog": { + "678c780a2c": "Disk space is unavailable", + "e2fcf07c0d": "Orca could not save this terminal session because local storage is full or not writable. Open the disk space analyzer to find workspace storage you can clean up." + }, "TerminalSshReconnectOverlay": { "connectButton": "Connect", "connectingButton": "Connecting..." @@ -2687,16 +2700,18 @@ "notAgent": { "subtitle": "This terminal is not running a recognized coding agent.", "title": "No conversation here" - }, - "reconnecting": "Reconnecting to this chat…" + } }, "status": { "responding": "Agent is responding", - "thinking": "Thinking", "toggleDetails": "Toggle turn details", "workedFor": "Worked for {{value0}}", "workingFor": "Working for {{value0}}" }, + "subagents": { + "unnamed": "Subagent", + "writtenBy": "Written by subagent {{value0}}" + }, "tool": { "countN": "{{value0}} tool calls", "countOne": "1 tool call", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index ffdcd6031c2..9351961c28a 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -616,7 +616,8 @@ "zcode_label": "ZCode", "d443a47995": "Prime Agent", "mimo_code_label": "MiMo Code", - "opencode2_label": "OpenCode 2" + "opencode2_label": "OpenCode 2", + "codebuddy_label": "CodeBuddy" }, "skill": { "cli": { @@ -1727,7 +1728,6 @@ "notConnected": "Not connected", "notePasteTooLarge": "Paste is too large for the note field.", "waitForSetupBeforeAgent": "Wait for setup to complete before starting agent", - "waitForSetupBeforeAgentHelp": "Turn this on when setup installs dependencies, MCP servers, or config files the agent needs during startup.", "destroyDisabled": "destroy disabled", "destroyConfigured": "destroy configured", "noDestroyConfigured": "no destroy", @@ -8054,8 +8054,7 @@ "81057d5f71": "Saving...", "da37d6f10e": "Copy", "3149964b66": "Copied", - "waitForSetupBeforeAgent": "Wait for setup to complete before starting agent", - "waitForSetupBeforeAgentHelp": "Turn this on when setup installs dependencies, MCP servers, or config files the agent needs during startup." + "waitForSetupBeforeAgent": "Wait for setup to complete before starting agent" }, "RepositoryIconPicker": { "2b7d27b93c": "Use {{value0}} repo color", @@ -17837,7 +17836,9 @@ "startedN": "Kicked off {{value0}} subagents", "ranOne": "Ran 1 subagent", "ranN": "Ran {{value0}} subagents", - "tokens": "{{value0}} tokens" + "tokens": "{{value0}} tokens", + "unnamed": "Subagent", + "writtenBy": "Written by subagent {{value0}}" }, "conversationCommand": { "pendingWork": "Wait for pending work and messages to finish before using this command.", @@ -18498,5 +18499,12 @@ }, "common": { "retry": "Retry" + }, + "terminal": { + "sessionSaveFailure": { + "title": "Could not save terminal session", + "capacity": "The device saving this terminal reported full storage or an exceeded storage quota. Free space on that device, then try again.", + "unknown": "Orca could not save the terminal state. Try again. If this continues, share the Orca logs with support so we can identify the cause." + } } } diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index adbe527574a..e0e77e574ec 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -385,7 +385,8 @@ "d443a47995": "Prime Agent", "mimo_code_label": "MiMo Code", "muse_label": "Muse", - "zcode_label": "ZCode" + "zcode_label": "ZCode", + "codebuddy_label": "CodeBuddy" }, "skill": { "cli": { @@ -1378,7 +1379,6 @@ "reuseExistingBranchHint": "Haz checkout de la rama existente en lugar de crear una nueva a partir de ella.", "createMultiple": "Crear más", "waitForSetupBeforeAgent": "Esperar a que termine el setup antes de iniciar el agente", - "waitForSetupBeforeAgentHelp": "Activa esto cuando el setup instale dependencias, servidores MCP o archivos de configuración que el agente necesita al iniciar.", "destroyDisabled": "destroy deshabilitado", "destroyConfigured": "destroy configurado", "noDestroyConfigured": "sin destroy", @@ -6803,8 +6803,7 @@ "81057d5f71": "Guardando...", "da37d6f10e": "Copiar", "3149964b66": "Copiado", - "waitForSetupBeforeAgent": "Espera a que se complete la configuración antes de iniciar el agente", - "waitForSetupBeforeAgentHelp": "Activa esto cuando la configuración instale dependencias, servidores MCP o archivos de configuración que el agente necesite durante el inicio." + "waitForSetupBeforeAgent": "Espera a que se complete la configuración antes de iniciar el agente" }, "RepositoryIconPicker": { "2b7d27b93c": "Usar {{value0}} como color del repositorio", @@ -15104,6 +15103,10 @@ "label": "Contexto: {{used}} de {{window}} tokens, {{percent}} % usados", "title": "Contexto", "estimated": "Estimado a partir de la última respuesta." + }, + "subagents": { + "unnamed": "Subagente", + "writtenBy": "Escrito por el subagente {{value0}}" } }, "tab": { diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 047564c4e61..9a0f0d437c9 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -595,6 +595,7 @@ "mimo_code_label": "MiMo Code", "muse_label": "Muse", "opencode2_label": "OpenCode 2", + "codebuddy_label": "CodeBuddy", "zcode_label": "ZCode" }, "skill": { @@ -1705,7 +1706,6 @@ "notConnected": "Non connecté", "notePasteTooLarge": "Le contenu collé est trop volumineux pour le champ de note.", "waitForSetupBeforeAgent": "Attendre la fin du setup avant de démarrer l'agent", - "waitForSetupBeforeAgentHelp": "Activez cette option quand le setup installe des dépendances, des serveurs MCP ou des fichiers de configuration dont l'agent a besoin au démarrage.", "destroyDisabled": "destroy désactivé", "destroyConfigured": "destroy configuré", "noDestroyConfigured": "pas de destroy", @@ -7990,8 +7990,7 @@ "81057d5f71": "Enregistrement...", "da37d6f10e": "Copier", "3149964b66": "Copied", - "waitForSetupBeforeAgent": "Attendre la fin du setup avant de démarrer l'agent", - "waitForSetupBeforeAgentHelp": "Activez cette option quand le setup installe des dépendances, des serveurs MCP ou des fichiers de configuration dont l'agent a besoin au démarrage." + "waitForSetupBeforeAgent": "Attendre la fin du setup avant de démarrer l'agent" }, "RepositoryIconPicker": { "2b7d27b93c": "Utiliser la couleur de dépôt {{value0}}", @@ -17769,7 +17768,9 @@ "startedN": "Lancement des sous-agents {{value0}}", "ranOne": "A exécuté 1 sous-agent", "ranN": "Ran les sous-agents {{value0}}", - "tokens": "Jetons {{value0}}" + "tokens": "Jetons {{value0}}", + "unnamed": "Sous-agent", + "writtenBy": "Écrit par le sous-agent {{value0}}" }, "conversationCommand": { "pendingWork": "Attendez la fin du travail et des messages en attente avant d'utiliser cette commande.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 97ff2a6ffda..6ec0e9c65e7 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -496,6 +496,7 @@ "mimo_code_label": "MiMo Code", "muse_label": "Muse", "opencode2_label": "OpenCode 2", + "codebuddy_label": "CodeBuddy", "zcode_label": "ZCode" }, "skill": { @@ -1602,7 +1603,6 @@ "reuseExistingBranchHint": "既存のブランチから新規ブランチを作成せず、そのブランチをチェックアウトします。", "createMultiple": "さらに作成", "waitForSetupBeforeAgent": "セットアップが完了するまで待ってから Agent を開始してください", - "waitForSetupBeforeAgentHelp": "セットアップによって、起動時に Agent に必要な依存関係、MCP サーバー、または構成ファイルがインストールされるときに、これをオンにします。", "destroyDisabled": "破壊は無効です", "destroyConfigured": "設定されたものを破棄する", "noDestroyConfigured": "破壊しない", @@ -7871,8 +7871,7 @@ "81057d5f71": "保存中…", "da37d6f10e": "コピー", "3149964b66": "コピーしました", - "waitForSetupBeforeAgent": "セットアップが完了するまで待ってから Agent を開始してください", - "waitForSetupBeforeAgentHelp": "セットアップによって、起動時に Agent に必要な依存関係、MCP サーバー、または構成ファイルがインストールされるときに、これをオンにします。" + "waitForSetupBeforeAgent": "セットアップが完了するまで待ってから Agent を開始してください" }, "RepositoryIconPicker": { "2b7d27b93c": "{{value0}} リポジトリカラーを使用する", @@ -17705,7 +17704,9 @@ "startedN": "{{value0}} サブ Agent をキックオフしました", "ranOne": "1 人のサブ Agent を実行しました", "ranN": "{{value0}} サブ Agent を実行しました", - "tokens": "{{value0}} トークン" + "tokens": "{{value0}} トークン", + "unnamed": "サブ Agent", + "writtenBy": "サブ Agent {{value0}} による出力" }, "conversationCommand": { "pendingWork": "このコマンドを使用する前に、保留中の作業とメッセージが完了するまで待ってください。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 511ee010e70..e5d5cac60fc 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -496,6 +496,7 @@ "mimo_code_label": "MiMo Code", "muse_label": "Muse", "opencode2_label": "OpenCode 2", + "codebuddy_label": "CodeBuddy", "zcode_label": "ZCode" }, "skill": { @@ -1602,7 +1603,6 @@ "reuseExistingBranchHint": "기존 브랜치에서 새 브랜치를 만들지 않고 해당 브랜치를 체크아웃합니다.", "createMultiple": "더 만들기", "waitForSetupBeforeAgent": "agent를 시작하기 전에 설정이 완료될 때까지 기다리세요.", - "waitForSetupBeforeAgentHelp": "설치 프로그램이 시작 중에 agent에 필요한 종속성, MCP 서버 또는 구성 파일을 설치할 때 이 기능을 켜십시오.", "destroyDisabled": "비활성화된 파괴", "destroyConfigured": "구성된 파괴", "noDestroyConfigured": "파괴하지 마세요", @@ -7834,8 +7834,7 @@ "81057d5f71": "저장 중...", "da37d6f10e": "복사", "3149964b66": "복사됨", - "waitForSetupBeforeAgent": "agent를 시작하기 전에 설정이 완료될 때까지 기다리세요.", - "waitForSetupBeforeAgentHelp": "설치 프로그램이 시작 중에 agent에 필요한 종속성, MCP 서버 또는 구성 파일을 설치할 때 이 기능을 켜십시오." + "waitForSetupBeforeAgent": "agent를 시작하기 전에 설정이 완료될 때까지 기다리세요." }, "RepositoryIconPicker": { "2b7d27b93c": "{{value0}} repo 색상 사용", @@ -17705,7 +17704,9 @@ "startedN": "{{value0}} 하위 에이전트가 시작되었습니다.", "ranOne": "하위 에이전트 1개 실행", "ranN": "{{value0}} 하위 에이전트를 실행했습니다.", - "tokens": "{{value0}} 토큰" + "tokens": "{{value0}} 토큰", + "unnamed": "하위 에이전트", + "writtenBy": "하위 에이전트 {{value0}}의 출력" }, "conversationCommand": { "pendingWork": "이 명령을 사용하기 전에 보류 중인 작업과 메시지가 완료될 때까지 기다리십시오.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index e4fa1521899..d8a7ebf4561 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -496,6 +496,7 @@ "mimo_code_label": "MiMo Code", "muse_label": "Muse", "opencode2_label": "OpenCode 2", + "codebuddy_label": "CodeBuddy", "zcode_label": "ZCode" }, "skill": { @@ -1602,7 +1603,6 @@ "reuseExistingBranchHint": "检出已有分支,而不是从它创建新分支。", "createMultiple": "创建更多", "waitForSetupBeforeAgent": "等待设置完成后再启动智能体", - "waitForSetupBeforeAgentHelp": "如果设置会安装依赖、MCP 服务器或智能体启动时需要的配置文件,请开启此选项。", "destroyDisabled": "已禁用销毁", "destroyConfigured": "已配置销毁", "noDestroyConfigured": "未配置销毁", @@ -7834,8 +7834,7 @@ "81057d5f71": "保存中...", "da37d6f10e": "复制", "3149964b66": "已复制", - "waitForSetupBeforeAgent": "等待设置完成后再启动智能体", - "waitForSetupBeforeAgentHelp": "如果设置会安装依赖、MCP 服务器或智能体启动时需要的配置文件,请开启此选项。" + "waitForSetupBeforeAgent": "等待设置完成后再启动智能体" }, "RepositoryIconPicker": { "2b7d27b93c": "使用 {{value0}} 仓库颜色", @@ -17670,7 +17669,9 @@ "startedN": "启动 {{value0}} 子代理", "ranOne": "Ran 1 子代理", "ranN": "Ran {{value0}} 子代理", - "tokens": "{{value0}} 代币" + "tokens": "{{value0}} 代币", + "unnamed": "子代理", + "writtenBy": "由子代理 {{value0}} 输出" }, "conversationCommand": { "pendingWork": "在使用此命令之前,请等待挂起的工作和消息完成。", diff --git a/src/renderer/src/lib/agent-catalog-platform.ts b/src/renderer/src/lib/agent-catalog-platform.ts new file mode 100644 index 00000000000..9a9eac351f0 --- /dev/null +++ b/src/renderer/src/lib/agent-catalog-platform.ts @@ -0,0 +1,13 @@ +export function getCatalogPlatform(): NodeJS.Platform { + const userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent + if (userAgent.includes('Windows')) { + return 'win32' + } + if (userAgent.includes('Mac')) { + return 'darwin' + } + if (userAgent) { + return 'linux' + } + return typeof process === 'undefined' ? 'linux' : process.platform +} diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx index 56cd2180c72..325d838552d 100644 --- a/src/renderer/src/lib/agent-catalog.tsx +++ b/src/renderer/src/lib/agent-catalog.tsx @@ -1,3 +1,4 @@ +import { getCatalogPlatform } from './agent-catalog-platform' import type React from 'react' import { ClaudeIcon, DroidIcon, OpenAIIcon } from '@/components/status-bar/icons' import openClaudeLogoUrl from '../../../../resources/openclaude-logo.png?url' @@ -30,20 +31,6 @@ export type AgentCatalogEntry = { homepageUrl: string } -function getCatalogPlatform(): NodeJS.Platform { - const userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent - if (userAgent.includes('Windows')) { - return 'win32' - } - if (userAgent.includes('Mac')) { - return 'darwin' - } - if (userAgent) { - return 'linux' - } - return typeof process === 'undefined' ? 'linux' : process.platform -} - export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => [ { id: 'claude', @@ -349,6 +336,13 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => cmd: 'openclaw', faviconDomain: 'openclaw.ai', homepageUrl: 'https://github.com/openclaw/openclaw' + }, + { + id: 'codebuddy', + label: translate('auto.lib.agent.catalog.codebuddy_label', 'CodeBuddy'), + cmd: 'codebuddy', + faviconDomain: 'codebuddy.ai', + homepageUrl: 'https://www.codebuddy.ai/cli' } ]) diff --git a/src/renderer/src/lib/agent-status.ts b/src/renderer/src/lib/agent-status.ts index 41ccc2cc7d7..04ecd728541 100644 --- a/src/renderer/src/lib/agent-status.ts +++ b/src/renderer/src/lib/agent-status.ts @@ -100,6 +100,7 @@ export { formatAgentTypeLabel } from '../../../shared/agent-type-label' const ICONABLE_AGENT_TYPES: Record = { claude: true, 'claude-agent-teams': true, + codebuddy: true, openclaude: true, codex: true, autohand: true, diff --git a/src/renderer/src/lib/delayed-status.test.ts b/src/renderer/src/lib/delayed-status.test.ts new file mode 100644 index 00000000000..11778a3d414 --- /dev/null +++ b/src/renderer/src/lib/delayed-status.test.ts @@ -0,0 +1,89 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createDelayedStatus, STATUS_MIN_VISIBLE_MS, type ShownStatus } from './delayed-status' + +const SHOW_DELAY_MS = 1_000 + +function track() { + const changes: (ShownStatus | null)[] = [] + const status = createDelayedStatus((shown) => changes.push(shown), { + showDelayMs: SHOW_DELAY_MS + }) + return { status, changes } +} + +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +describe('createDelayedStatus', () => { + it('never shows a status that clears within the show delay', () => { + const { status, changes } = track() + status.update('a', 'starting') + vi.advanceTimersByTime(SHOW_DELAY_MS - 1) + status.update('a', null) + vi.advanceTimersByTime(SHOW_DELAY_MS * 2) + expect(changes).toEqual([]) + }) + + it('shows a status that outlasts the show delay', () => { + const { status, changes } = track() + status.update('a', 'starting') + vi.advanceTimersByTime(SHOW_DELAY_MS) + expect(changes).toEqual([{ key: 'a', value: 'starting' }]) + }) + + it('keeps a shown status up for the minimum visible time', () => { + const { status, changes } = track() + status.update('a', 'starting') + vi.advanceTimersByTime(SHOW_DELAY_MS) + status.update('a', null) + vi.advanceTimersByTime(STATUS_MIN_VISIBLE_MS - 1) + expect(changes).toEqual([{ key: 'a', value: 'starting' }]) + vi.advanceTimersByTime(1) + expect(changes).toEqual([{ key: 'a', value: 'starting' }, null]) + }) + + it('hides at once when the status clears after the minimum visible time', () => { + const { status, changes } = track() + status.update('a', 'starting') + vi.advanceTimersByTime(SHOW_DELAY_MS + STATUS_MIN_VISIBLE_MS) + status.update('a', null) + expect(changes).toEqual([{ key: 'a', value: 'starting' }, null]) + }) + + it('restarts the show delay when an unkeyed status clears and returns during its hold', () => { + const { status, changes } = track() + status.update('a', 'starting') + vi.advanceTimersByTime(SHOW_DELAY_MS) + status.update('a', null) + vi.advanceTimersByTime(100) + status.update('a', 'starting') + expect(changes).toEqual([{ key: 'a', value: 'starting' }, null]) + vi.advanceTimersByTime(SHOW_DELAY_MS - 1) + expect(changes).toHaveLength(2) + vi.advanceTimersByTime(1) + expect(changes.at(-1)).toEqual({ key: 'a', value: 'starting' }) + }) + + it('drops the shown status at once for a new key, which waits its own delay', () => { + const { status, changes } = track() + status.update('a', 'starting') + vi.advanceTimersByTime(SHOW_DELAY_MS) + status.update('b', 'starting') + expect(changes).toEqual([{ key: 'a', value: 'starting' }, null]) + vi.advanceTimersByTime(SHOW_DELAY_MS) + expect(changes.at(-1)).toEqual({ key: 'b', value: 'starting' }) + }) + + it('does not show after dispose', () => { + const { status, changes } = track() + status.update('a', 'starting') + status.dispose() + vi.advanceTimersByTime(SHOW_DELAY_MS * 2) + expect(changes).toEqual([]) + }) +}) diff --git a/src/renderer/src/lib/delayed-status.ts b/src/renderer/src/lib/delayed-status.ts new file mode 100644 index 00000000000..8c388c966e4 --- /dev/null +++ b/src/renderer/src/lib/delayed-status.ts @@ -0,0 +1,92 @@ +/** How long a shown status stays up, so it cannot flash right at the show delay. */ +export const STATUS_MIN_VISIBLE_MS = 400 + +/** The status a view should show, and the key it belongs to. */ +export type ShownStatus = { + key: string + value: A +} + +export type DelayedStatus = { + /** Reports the real status for `key`. A new key drops the shown status at once. */ + update: (key: string, value: A | null) => void + /** Cancels pending timers. A later `update` starts again from the real status. */ + dispose: () => void +} + +/** + * Turns a real status into the status a view shows. A status that clears within + * `showDelayMs` is never shown; a shown status stays for at least `minVisibleMs`. + * Values compare by identity, so use strings or other stable values. + */ +export function createDelayedStatus( + onChange: (shown: ShownStatus | null) => void, + options: { showDelayMs: number; minVisibleMs?: number } +): DelayedStatus { + const minVisibleMs = options.minVisibleMs ?? STATUS_MIN_VISIBLE_MS + let key = '' + let latest: A | null = null + let shown: A | null = null + // While hidden, this is the show delay. While shown, the minimum visible time. + let timer: ReturnType | undefined + + const clearTimer = (): void => { + clearTimeout(timer) + timer = undefined + } + const hide = (): void => { + shown = null + onChange(null) + } + const show = (value: A): void => { + shown = value + onChange({ key, value }) + clearTimer() + timer = setTimeout(() => { + timer = undefined + if (latest === null) { + hide() + } + }, minVisibleMs) + } + + return { + update: (nextKey, value) => { + if (nextKey !== key) { + key = nextKey + clearTimer() + if (shown !== null) { + hide() + } + } + if (shown !== null && latest === null && value !== null) { + clearTimer() + hide() + } + latest = value + + if (shown === null) { + if (value === null) { + clearTimer() + } else if (timer === undefined) { + timer = setTimeout(() => { + timer = undefined + if (latest !== null) { + show(latest) + } + }, options.showDelayMs) + } + return + } + + if (value !== null) { + if (value !== shown) { + show(value) + } + } else if (timer === undefined) { + hide() + } + }, + dispose: clearTimer + } +} diff --git a/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts b/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts index 24393546a81..e5b6d8327ae 100644 --- a/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts +++ b/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { RESUMABLE_TUI_AGENTS } from '../../../shared/agent-session-resume' import { AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY, + AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY, @@ -56,6 +57,7 @@ describe('agentResumeHostAuthorityCapability', () => { ) ).toEqual({ claude: undefined, + codebuddy: AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, codex: undefined, qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY, gemini: undefined, diff --git a/src/renderer/src/runtime/agent-resume-host-authority-capability.ts b/src/renderer/src/runtime/agent-resume-host-authority-capability.ts index efec3fe38b1..a5cefe6a74c 100644 --- a/src/renderer/src/runtime/agent-resume-host-authority-capability.ts +++ b/src/renderer/src/runtime/agent-resume-host-authority-capability.ts @@ -3,6 +3,7 @@ import type { TuiAgent } from '../../../shared/tui-agent' import { AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY, + AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY, @@ -20,6 +21,7 @@ import { const RESUME_HOST_AUTHORITY_CAPABILITY_BY_AGENT = { // These shipped inside agent-session.host-authority.v1's enum, so the generic probe covers them. claude: undefined, + codebuddy: AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, codex: undefined, qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY, gemini: undefined, diff --git a/src/renderer/src/runtime/remote-agent-session-launch.test.ts b/src/renderer/src/runtime/remote-agent-session-launch.test.ts index d8cf0c542cb..dda22ff12fd 100644 --- a/src/renderer/src/runtime/remote-agent-session-launch.test.ts +++ b/src/renderer/src/runtime/remote-agent-session-launch.test.ts @@ -45,7 +45,7 @@ describe('remote agent-session launch routing', () => { expect(legacy).not.toHaveBeenCalled() }) - it.each(['kimi', 'muse'] as const)( + it.each(['kimi', 'muse', 'codebuddy'] as const)( 'falls back to legacy when an older host lacks the %s resume capability', async (agent) => { const hostAuthority = vi.fn().mockResolvedValue('structured') @@ -70,23 +70,29 @@ describe('remote agent-session launch routing', () => { } ) - it('uses host authority when the host supports Muse resume', async () => { - const hostAuthority = vi.fn().mockResolvedValue('host') - const legacy = vi.fn() - mocks.supportsCapability.mockResolvedValue(true) + it.each(['muse', 'codebuddy'] as const)( + 'uses host authority when the host supports %s resume', + async (agent) => { + const hostAuthority = vi.fn().mockResolvedValue('host') + const legacy = vi.fn() + mocks.supportsCapability.mockResolvedValue(true) - await expect( - runRemoteAgentSessionLaunch({ - environmentId: 'env-1', - hostAuthority, - hostAuthorityCapability: agentResumeHostAuthorityCapability('muse'), - legacy - }) - ).resolves.toBe('host') - expect(mocks.supportsCapability).toHaveBeenCalledWith('env-1', 'agent-session.muse-resume.v1') - expect(hostAuthority).toHaveBeenCalledOnce() - expect(legacy).not.toHaveBeenCalled() - }) + await expect( + runRemoteAgentSessionLaunch({ + environmentId: 'env-1', + hostAuthority, + hostAuthorityCapability: agentResumeHostAuthorityCapability(agent), + legacy + }) + ).resolves.toBe('host') + expect(mocks.supportsCapability).toHaveBeenCalledWith( + 'env-1', + `agent-session.${agent}-resume.v1` + ) + expect(hostAuthority).toHaveBeenCalledOnce() + expect(legacy).not.toHaveBeenCalled() + } + ) it('preserves the exact legacy path when the capability is absent', async () => { const hostAuthority = vi.fn().mockResolvedValue('structured') diff --git a/src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts b/src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts index 4f98700d806..e192bddae52 100644 --- a/src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts +++ b/src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts @@ -27,7 +27,9 @@ const owned: AgentSessionStatusSummary = { status: 'working', latestPrompt: 'work', updatedAt: 1, - hostExecutionOwned: true + hostExecutionOwned: true, + hostExecutionPhase: 'starting', + hostExecutionChild: { generation: 'child-1', fence: 1 } } const done: AgentSessionStatusSummary = { ...owned, @@ -69,7 +71,12 @@ describe('structured status feed execution authority lifecycle', () => { subscription().emit({ type: 'snapshot', sessions: [owned, done] }) subscription().emit({ type: 'end' }) expect(subscription().unsubscribe).toHaveBeenCalledOnce() - expect(feed.getSnapshot().get('running')).toEqual({ ...owned, hostExecutionOwned: undefined }) + expect(feed.getSnapshot().get('running')).toEqual({ + ...owned, + hostExecutionOwned: undefined, + hostExecutionPhase: undefined, + hostExecutionChild: undefined + }) expect(feed.getSnapshot().get('completed')).toBe(done) subscription().emit({ type: 'status', session: owned }) expect(feed.getSnapshot().get('running')?.hostExecutionOwned).toBeUndefined() diff --git a/src/renderer/src/runtime/structured-agent-session-status-feed.ts b/src/renderer/src/runtime/structured-agent-session-status-feed.ts index 84bbcacdecc..7e4d7f27eb8 100644 --- a/src/renderer/src/runtime/structured-agent-session-status-feed.ts +++ b/src/renderer/src/runtime/structured-agent-session-status-feed.ts @@ -95,7 +95,12 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed { if (!next) { next = new Map(snapshot) } - const { hostExecutionOwned: _owned, hostExecutionPhase: _phase, ...retained } = summary + const { + hostExecutionOwned: _owned, + hostExecutionPhase: _phase, + hostExecutionChild: _child, + ...retained + } = summary next.set(sessionId, retained) } if (next) { diff --git a/src/shared/__fixtures__/codebuddy-question-hooks.jsonl b/src/shared/__fixtures__/codebuddy-question-hooks.jsonl new file mode 100644 index 00000000000..81a69a899b6 --- /dev/null +++ b/src/shared/__fixtures__/codebuddy-question-hooks.jsonl @@ -0,0 +1,6 @@ +{"hook_event_name": "UserPromptSubmit", "prompt": "Use AskUserQuestion to ask me to choose red or blue. After I answer, reply with my color and stop.", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"} +{"hook_event_name": "SessionStart", "source": "startup", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"} +{"hook_event_name": "Notification", "notification_type": "permission_prompt", "message": "needs your permission to use AskUserQuestion", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"} +{"hook_event_name": "PreToolUse", "tool_name": "AskUserQuestion", "tool_input": {"questions": [{"header": "Color", "multiSelect": false, "options": [{"description": "Choose red", "label": "Red"}, {"description": "Choose blue", "label": "Blue"}], "question": "Which color do you choose?"}]}, "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"} +{"hook_event_name": "PostToolUse", "tool_name": "AskUserQuestion", "tool_input": {"questions": [{"header": "Color", "multiSelect": false, "options": [{"description": "Choose red", "label": "Red"}, {"description": "Choose blue", "label": "Blue"}], "question": "Which color do you choose?"}]}, "tool_response": " \u00b7 Which color do you choose? \u2192 Red", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"} +{"hook_event_name": "Stop", "last_assistant_message": "Red", "permission_mode": "default", "version": "2.159.0", "model": "fast-model", "session_id": "codebuddy-verification", "transcript_path": "/sessions/codebuddy-verification.jsonl"} diff --git a/src/shared/__fixtures__/codebuddy-question-hooks.meta.json b/src/shared/__fixtures__/codebuddy-question-hooks.meta.json new file mode 100644 index 00000000000..28ac3a2d76b --- /dev/null +++ b/src/shared/__fixtures__/codebuddy-question-hooks.meta.json @@ -0,0 +1,6 @@ +{ + "version": "2.159.0", + "platform": "darwin", + "model": "fast-model", + "description": "Live CLI: question notification precedes PreToolUse; PreToolUse follows the answer; SessionStart follows UserPromptSubmit. Host paths and session identifiers replaced." +} diff --git a/src/shared/agent-headless-command.ts b/src/shared/agent-headless-command.ts index f3f67535963..9f742a5355d 100644 --- a/src/shared/agent-headless-command.ts +++ b/src/shared/agent-headless-command.ts @@ -1,3 +1,4 @@ +import { isCodebuddyNonInteractiveCommand } from './codebuddy-headless-command' import { isQoderHeadlessCommand } from './qoder-headless-command' import { isAnteHeadlessOneShotCommand } from './ante-headless-command' import { isDshNonInteractiveCommand } from './dsh-launch-command' @@ -18,6 +19,7 @@ const HEADLESS_ONE_SHOT_MATCHERS: Partial< > = { qoder: isQoderHeadlessCommand, claude: isPrintModeHeadlessOneShotCommand, + codebuddy: isCodebuddyNonInteractiveCommand, trae: isPrintModeHeadlessOneShotCommand, 'prime-agent': isPrimeAgentHeadlessOneShotCommand, ante: isAnteHeadlessOneShotCommand, diff --git a/src/shared/agent-hook-listener/provider-dispatch.ts b/src/shared/agent-hook-listener/provider-dispatch.ts index 067e4552212..ab7e847c4ca 100644 --- a/src/shared/agent-hook-listener/provider-dispatch.ts +++ b/src/shared/agent-hook-listener/provider-dispatch.ts @@ -1,3 +1,4 @@ +import { normalizeCompatibleLifecycleEvent } from './providers/compatible-lifecycle-events' import { normalizeQoderEvent } from './providers/qoder-events' import type { ParsedAgentStatusPayload } from '../agent-status-types' import type { AgentHookSource } from '../agent-hook-relay' @@ -53,6 +54,16 @@ export function normalizeProviderEvent(input: { let payload: ParsedAgentStatusPayload | null switch (source) { + case 'codebuddy': + payload = normalizeCompatibleLifecycleEvent( + source, + state, + eventName, + promptText, + paneKey, + hookPayload + ) + break case 'claude': payload = normalizeClaudeEvent(state, eventName, promptText, paneKey, hookPayload) break diff --git a/src/shared/agent-hook-listener/provider-event-routing.ts b/src/shared/agent-hook-listener/provider-event-routing.ts index 40e59a1d820..80cd0fdc91c 100644 --- a/src/shared/agent-hook-listener/provider-event-routing.ts +++ b/src/shared/agent-hook-listener/provider-event-routing.ts @@ -30,6 +30,7 @@ export function isNewTurnEvent(source: AgentHookSource, eventName: unknown): boo // Why: SessionStart lands an idle row (STA-3386) and must also drop stale // tool/prompt caches left by the pane's previous session. return eventName === 'SessionStart' || eventName === 'UserPromptSubmit' + case 'codebuddy': case 'kimi': // Why: Kimi Code emits Claude-compatible hook events, so UserPromptSubmit is its new-turn boundary too. return eventName === 'UserPromptSubmit' @@ -143,6 +144,7 @@ export function extractToolFields( case 'claude': // Why: Kimi Code uses Claude's tool_name/tool_input payload fields verbatim. // falls through + case 'codebuddy': case 'kimi': // Muse uses Claude-compatible tool fields. // falls through diff --git a/src/shared/agent-hook-listener/providers/compatible-lifecycle-events.ts b/src/shared/agent-hook-listener/providers/compatible-lifecycle-events.ts new file mode 100644 index 00000000000..2678dfc5e1e --- /dev/null +++ b/src/shared/agent-hook-listener/providers/compatible-lifecycle-events.ts @@ -0,0 +1,104 @@ +import { isAskUserQuestionTool } from '../../agent-question-answered-intent' +import { + normalizeAgentStatusPayload, + type ParsedAgentStatusPayload +} from '../../agent-status-types' +import type { HookListenerState } from '../listener-state' +import { + resolvePrompt, + resolveToolState, + shouldIgnoreCompactContinuationUserPromptSubmit +} from '../prompt-fields' +import { extractToolFields, isNewTurnEvent } from '../provider-event-routing' +import { readString } from '../tool-input-preview' + +function readCompatibleState( + eventName: unknown, + payload: Record +): 'working' | 'waiting' | 'done' | null { + switch (eventName) { + case 'SessionStart': + // Compaction restarts the session during an active turn. + return ['startup', 'resume', 'clear', 'new'].includes(String(payload.source)) ? 'done' : null + case 'UserPromptSubmit': + case 'PostToolUse': + case 'PostToolUseFailure': + return 'working' + case 'PreToolUse': + return isAskUserQuestionTool(readString(payload, 'tool_name')) ? 'waiting' : 'working' + case 'PermissionRequest': + return 'waiting' + case 'Stop': + case 'StopFailure': + case 'SessionEnd': + return 'done' + case 'PostCompact': + return payload.trigger === 'manual' ? 'done' : null + case 'Notification': + if ( + payload.notification_type === 'permission_prompt' || + payload.notification_type === 'elicitation_dialog' + ) { + return 'waiting' + } + return payload.notification_type === 'idle_prompt' ? 'done' : null + default: + return null + } +} + +export function normalizeCompatibleLifecycleEvent( + source: 'qoder' | 'codebuddy', + state: HookListenerState, + eventName: unknown, + promptText: string, + paneKey: string, + hookPayload: Record +): ParsedAgentStatusPayload | null { + if (shouldIgnoreCompactContinuationUserPromptSubmit(eventName, promptText)) { + return null + } + // CodeBuddy emits SessionStart after UserPromptSubmit, so it cannot settle a turn. + if (source === 'codebuddy' && eventName === 'SessionStart') { + return null + } + const stateName = + source === 'codebuddy' && eventName === 'PreToolUse' + ? 'working' + : readCompatibleState(eventName, hookPayload) + if (!stateName) { + return null + } + const resetOnNewTurn = isNewTurnEvent(source, eventName) + const toolFields = extractToolFields(source, eventName, hookPayload) + if ( + source === 'codebuddy' && + eventName === 'Notification' && + hookPayload.notification_type === 'permission_prompt' && + hookPayload.message === 'needs your permission to use AskUserQuestion' + ) { + toolFields.toolName = 'AskUserQuestion' + } + const snapshot = resolveToolState(state, paneKey, toolFields, { resetOnNewTurn }) + return normalizeAgentStatusPayload({ + state: stateName, + agentType: source, + prompt: resolvePrompt( + state, + paneKey, + source === 'codebuddy' && eventName === 'Notification' ? '' : promptText, + { resetOnNewTurn } + ), + toolName: snapshot.toolName, + toolInput: snapshot.toolInput, + interactivePrompt: snapshot.interactivePrompt, + lastAssistantMessage: snapshot.lastAssistantMessage, + lastAssistantMessageIsToolOutput: snapshot.lastAssistantMessageIsToolOutput, + sessionBoundary: eventName === 'SessionStart' || eventName === 'PostCompact' ? true : undefined, + interrupted: eventName === 'Stop' && hookPayload.is_interrupt === true ? true : undefined, + mainAgent: + eventName === 'StopFailure' + ? { state: 'done', outcome: 'failure', stateStartedAt: Date.now() } + : undefined + }) +} diff --git a/src/shared/agent-hook-listener/providers/qoder-events.ts b/src/shared/agent-hook-listener/providers/qoder-events.ts index bdf2a622dbe..c83923d58f7 100644 --- a/src/shared/agent-hook-listener/providers/qoder-events.ts +++ b/src/shared/agent-hook-listener/providers/qoder-events.ts @@ -1,51 +1,5 @@ -import { isAskUserQuestionTool } from '../../agent-question-answered-intent' -import { - normalizeAgentStatusPayload, - type ParsedAgentStatusPayload -} from '../../agent-status-types' +import { normalizeCompatibleLifecycleEvent } from './compatible-lifecycle-events' import type { HookListenerState } from '../listener-state' -import { - resolvePrompt, - resolveToolState, - shouldIgnoreCompactContinuationUserPromptSubmit -} from '../prompt-fields' -import { extractToolFields, isNewTurnEvent } from '../provider-event-routing' -import { readString } from '../tool-input-preview' - -function readQoderState( - eventName: unknown, - payload: Record -): 'working' | 'waiting' | 'done' | null { - switch (eventName) { - case 'SessionStart': - // Compaction restarts the session during an active turn. - return ['startup', 'resume', 'clear', 'new'].includes(String(payload.source)) ? 'done' : null - case 'UserPromptSubmit': - case 'PostToolUse': - case 'PostToolUseFailure': - return 'working' - case 'PreToolUse': - return isAskUserQuestionTool(readString(payload, 'tool_name')) ? 'waiting' : 'working' - case 'PermissionRequest': - return 'waiting' - case 'Stop': - case 'StopFailure': - case 'SessionEnd': - return 'done' - case 'PostCompact': - return payload.trigger === 'manual' ? 'done' : null - case 'Notification': - if ( - payload.notification_type === 'permission_prompt' || - payload.notification_type === 'elicitation_dialog' - ) { - return 'waiting' - } - return payload.notification_type === 'idle_prompt' ? 'done' : null - default: - return null - } -} export function normalizeQoderEvent( state: HookListenerState, @@ -53,35 +7,13 @@ export function normalizeQoderEvent( promptText: string, paneKey: string, hookPayload: Record -): ParsedAgentStatusPayload | null { - if (shouldIgnoreCompactContinuationUserPromptSubmit(eventName, promptText)) { - return null - } - const stateName = readQoderState(eventName, hookPayload) - if (!stateName) { - return null - } - const resetOnNewTurn = isNewTurnEvent('qoder', eventName) - const snapshot = resolveToolState( +) { + return normalizeCompatibleLifecycleEvent( + 'qoder', state, + eventName, + promptText, paneKey, - extractToolFields('qoder', eventName, hookPayload), - { resetOnNewTurn } + hookPayload ) - return normalizeAgentStatusPayload({ - state: stateName, - agentType: 'qoder', - prompt: resolvePrompt(state, paneKey, promptText, { resetOnNewTurn }), - toolName: snapshot.toolName, - toolInput: snapshot.toolInput, - interactivePrompt: snapshot.interactivePrompt, - lastAssistantMessage: snapshot.lastAssistantMessage, - lastAssistantMessageIsToolOutput: snapshot.lastAssistantMessageIsToolOutput, - sessionBoundary: eventName === 'SessionStart' || eventName === 'PostCompact' ? true : undefined, - interrupted: eventName === 'Stop' && hookPayload.is_interrupt === true ? true : undefined, - mainAgent: - eventName === 'StopFailure' - ? { state: 'done', outcome: 'failure', stateStartedAt: Date.now() } - : undefined - }) } diff --git a/src/shared/agent-hook-listener/source-routing.ts b/src/shared/agent-hook-listener/source-routing.ts index 2b7f0147e60..2940685665c 100644 --- a/src/shared/agent-hook-listener/source-routing.ts +++ b/src/shared/agent-hook-listener/source-routing.ts @@ -6,6 +6,7 @@ export const HOOK_SOURCE_BY_PATHNAME: Readonly> '/hook/claude': 'claude', '/hook/codex': 'codex', '/hook/qoder': 'qoder', + '/hook/codebuddy': 'codebuddy', '/hook/gemini': 'gemini', '/hook/antigravity': 'antigravity', '/hook/amp': 'amp', diff --git a/src/shared/agent-hook-relay.ts b/src/shared/agent-hook-relay.ts index 7e0d452a66b..bb2dca99f72 100644 --- a/src/shared/agent-hook-relay.ts +++ b/src/shared/agent-hook-relay.ts @@ -38,6 +38,7 @@ const AGENT_HOOK_SOURCES = [ 'claude', 'codex', 'qoder', + 'codebuddy', 'gemini', 'antigravity', 'amp', diff --git a/src/shared/agent-hook-types.ts b/src/shared/agent-hook-types.ts index cc83527fc09..0bc5b67bc28 100644 --- a/src/shared/agent-hook-types.ts +++ b/src/shared/agent-hook-types.ts @@ -8,6 +8,7 @@ export const AGENT_HOOK_TARGETS = [ 'openclaude', 'codex', 'qoder', + 'codebuddy', 'gemini', 'antigravity', 'amp', diff --git a/src/shared/agent-icons/codebuddy.png b/src/shared/agent-icons/codebuddy.png new file mode 100644 index 00000000000..ecaab1216f4 Binary files /dev/null and b/src/shared/agent-icons/codebuddy.png differ diff --git a/src/shared/agent-kind.ts b/src/shared/agent-kind.ts index 375bac1bd2e..3fb112e6114 100644 --- a/src/shared/agent-kind.ts +++ b/src/shared/agent-kind.ts @@ -16,6 +16,7 @@ type ConcreteAgentKind = Exclude const TUI_AGENT_KIND_BY_AGENT = { claude: 'claude-code', 'claude-agent-teams': 'claude-agent-teams', + codebuddy: 'codebuddy', openclaude: 'openclaude', codex: 'codex', autohand: 'autohand', diff --git a/src/shared/agent-launch-intent.ts b/src/shared/agent-launch-intent.ts index 016b759d877..bffb93c1b9a 100644 --- a/src/shared/agent-launch-intent.ts +++ b/src/shared/agent-launch-intent.ts @@ -305,7 +305,9 @@ export const AGENT_LAUNCH_RESERVED_CREATE_FIELDS = [ 'startupDraft', 'startupLaunchConfig', 'startupEnv', - 'startupCommandDelivery' + 'startupCommandDelivery', + // The launch carries its own; a create's copy would be a second, possibly contradicting, answer. + 'launchSource' ] as const /** Strips the reserved agent fields from a create payload. Callers migrating from diff --git a/src/shared/agent-node-package-entrypoints.ts b/src/shared/agent-node-package-entrypoints.ts index 905d41f95f0..7f8f9abd7cb 100644 --- a/src/shared/agent-node-package-entrypoints.ts +++ b/src/shared/agent-node-package-entrypoints.ts @@ -6,6 +6,7 @@ // new agent's install path is a data edit, as in agent-node-entrypoint-identities.ts. export const NODE_PACKAGE_SCRIPT_ENTRYPOINTS: Record = { codex: ['node_modules/@openai/codex/'], + codebuddy: ['node_modules/@tencent-ai/codebuddy-code/'], gemini: ['node_modules/@google/gemini-cli/'], // Why: ZCode's npm bin is `dist/zcode.cjs`, so a package install runs as `node …zcode.cjs` // and never shows `zcode` as the foreground name (a SEA build still matches by name). diff --git a/src/shared/agent-session-journal-producer.ts b/src/shared/agent-session-journal-producer.ts index ff51c6eaf57..2a5410eb275 100644 --- a/src/shared/agent-session-journal-producer.ts +++ b/src/shared/agent-session-journal-producer.ts @@ -25,7 +25,15 @@ import type { export function isRootAgentJournalItem( item: Pick | undefined ): boolean { - return item?.agentId == null + return agentJournalItemSubagentId(item) === null +} + +/** The subagent that produced a row, or null when the session's own agent did. + * Same reading of absence as {@link isRootAgentJournalItem}. */ +export function agentJournalItemSubagentId( + item: Pick | undefined +): string | null { + return item?.agentId ?? null } /** Whether a write names its producer at all. One that does not revises a row diff --git a/src/shared/agent-session-lease-adjudication.test.ts b/src/shared/agent-session-lease-adjudication.test.ts index 602a263e91b..35e794efd0d 100644 --- a/src/shared/agent-session-lease-adjudication.test.ts +++ b/src/shared/agent-session-lease-adjudication.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest' import { adjudicateAgentSessionRestart, agentSessionLeaseAdmitsWriter, - classifyObservedAgentSessionSpawnToken, evaluateAgentSessionAcquisition, isProvenAliveProbe, isProvenDeadProbe, @@ -223,14 +222,28 @@ describe('restart reconciliation', () => { expect(result).toEqual({ disposition: 'evicted', nextFence: 8, + // The death happened between the last renewal and the probe. evidence: { kind: 'identity-mismatch', detail: 'mismatched process-start-time', - observedAt: 9_000 + observedAt: 9_000, + ownerFence: 7, + lastProvenAliveAt: 500 } }) }) + it('never records the owner alive after the probe that found it gone', () => { + // A clock stepped back across the restart would otherwise write an interval load rejects. + expect( + adjudicateAgentSessionRestart({ + lease: lease({ lastRenewedAt: 9_500 }), + probe: { outcome: 'pid-absent' }, + observedAt: 9_000 + }) + ).toMatchObject({ evidence: { observedAt: 9_000, lastProvenAliveAt: 9_000 } }) + }) + it('hands an unverifiable owner to recovery resolution instead of evicting it', () => { expect( adjudicateAgentSessionRestart({ @@ -270,7 +283,13 @@ describe('restart reconciliation', () => { ).toEqual({ disposition: 'evicted', nextFence: 8, - evidence: { kind: 'pid-absent', detail: 'recorded pid absent on host', observedAt: 9_000 } + evidence: { + kind: 'pid-absent', + detail: 'recorded pid absent on host', + observedAt: 9_000, + ownerFence: 7, + lastProvenAliveAt: 500 + } }) }) @@ -309,13 +328,19 @@ describe('restart reconciliation', () => { ).toEqual({ disposition: 'evicted', nextFence: 8, - evidence: { kind: 'pid-absent', detail: 'reservation never spawned', observedAt: 9_000 } + // The proof is about this reservation's own fence. + evidence: { + kind: 'pid-absent', + detail: 'reservation never spawned', + observedAt: 9_000, + ownerFence: 7 + } }) } ) }) -describe('writer admission and orphan spawn tokens', () => { +describe('writer admission', () => { it('admits a writer only when reconciled, settled, live, and holding a process', () => { expect(agentSessionLeaseAdmitsWriter(lease())).toBe(true) expect(agentSessionLeaseAdmitsWriter(lease({ unreconciled: true }))).toBe(false) @@ -323,18 +348,4 @@ describe('writer admission and orphan spawn tokens', () => { expect(agentSessionLeaseAdmitsWriter(lease({ claimStatus: 'reserved' }))).toBe(false) expect(agentSessionLeaseAdmitsWriter(lease({ ownerProcess: null }))).toBe(false) }) - - it('calls a spawn token with no matching lease an orphan', () => { - const leases = [lease(), lease({ sessionId: 'session-beta-1', reservedSpawnToken: 'spawn-b' })] - expect(classifyObservedAgentSessionSpawnToken({ spawnToken: 'spawn-a', leases })).toBe('owned') - expect(classifyObservedAgentSessionSpawnToken({ spawnToken: 'spawn-b', leases })).toBe('owned') - expect(classifyObservedAgentSessionSpawnToken({ spawnToken: 'spawn-z', leases })).toBe('orphan') - }) - - it('still recognises an owner whose reservation token was cleared after proving', () => { - const proved = lease({ reservedSpawnToken: null }) - expect( - classifyObservedAgentSessionSpawnToken({ spawnToken: 'spawn-a', leases: [proved] }) - ).toBe('owned') - }) }) diff --git a/src/shared/agent-session-lease-adjudication.ts b/src/shared/agent-session-lease-adjudication.ts index bbe409a37fc..271b136456f 100644 --- a/src/shared/agent-session-lease-adjudication.ts +++ b/src/shared/agent-session-lease-adjudication.ts @@ -80,16 +80,23 @@ export function isProvenAliveProbe(probe: AgentSessionOwnerProbe): boolean { function deathEvidenceFor( probe: AgentSessionOwnerProbe, - observedAt: number + observedAt: number, + lease: AgentSessionLease ): AgentSessionDeathEvidence | null { + // Capped so a clock that stepped back across the restart still writes a valid interval. + const interval = { + observedAt, + ownerFence: lease.runtimeFence, + lastProvenAliveAt: Math.min(lease.lastRenewedAt, observedAt) + } if (probe.outcome === 'exit-observed') { - return { kind: 'exit-observed', detail: 'observed process exit', observedAt } + return { kind: 'exit-observed', detail: 'observed process exit', ...interval } } if (probe.outcome === 'pid-absent') { - return { kind: 'pid-absent', detail: 'recorded pid absent on host', observedAt } + return { kind: 'pid-absent', detail: 'recorded pid absent on host', ...interval } } if (probe.outcome === 'identity-mismatch') { - return { kind: 'identity-mismatch', detail: `mismatched ${probe.field}`, observedAt } + return { kind: 'identity-mismatch', detail: `mismatched ${probe.field}`, ...interval } } return null } @@ -233,14 +240,19 @@ export function adjudicateAgentSessionRestart(args: { // Why: a child spawned before its identity was recorded lost its stdio with the runtime that // crashed, so nothing can drive it. A token scan that proves no child is the only evidence there // can be; without it the lease is released anyway. A live child still carrying the token is - // not signalled here, and the orphan reaper, which runs once at store open, may have seen this - // lease still claiming it. + // never signalled: the token is inherited by every descendant, so it cannot prove which one is + // the provider child. return { disposition: 'evicted', nextFence: nextAgentSessionFence(lease), evidence: probe.outcome === 'reservation-unused' - ? { kind: 'pid-absent', detail: 'reservation never spawned', observedAt } + ? { + kind: 'pid-absent', + detail: 'reservation never spawned', + observedAt, + ownerFence: lease.runtimeFence + } : null } } @@ -253,7 +265,7 @@ export function adjudicateAgentSessionRestart(args: { reason: 'owner outlived the runtime that held its transport' } } - const evidence = deathEvidenceFor(probe, observedAt) + const evidence = deathEvidenceFor(probe, observedAt, lease) if (evidence) { return { disposition: 'evicted', nextFence: nextAgentSessionFence(lease), evidence } } @@ -265,19 +277,3 @@ export function adjudicateAgentSessionRestart(args: { probe.outcome === 'indeterminate' ? probe.reason : 'process identity could not be verified' } } - -/** - * A process carrying an Orca spawn token with no matching lease is an orphan: stop it, never - * adopt it. Neither age nor CPU is evidence — only a token match justifies acting on a process. - */ -export function classifyObservedAgentSessionSpawnToken(args: { - spawnToken: string - leases: readonly AgentSessionLease[] -}): 'owned' | 'orphan' { - const owned = args.leases.some( - (lease) => - lease.reservedSpawnToken === args.spawnToken || - lease.ownerProcess?.spawnToken === args.spawnToken - ) - return owned ? 'owned' : 'orphan' -} diff --git a/src/shared/agent-session-option-catalog-codebuddy.ts b/src/shared/agent-session-option-catalog-codebuddy.ts new file mode 100644 index 00000000000..d0a8afa04c5 --- /dev/null +++ b/src/shared/agent-session-option-catalog-codebuddy.ts @@ -0,0 +1,44 @@ +import { hasFlag } from './agent-cli-flag-detection' +import { removeAgentArgOption } from './agent-session-option-agent-args' +import type { AgentSessionOptionCatalog, CatalogOption } from './agent-session-option-catalog-types' + +const CODEBUDDY_EFFORT: CatalogOption = { + id: 'effort', + label: 'Reasoning effort', + category: 'thought_level', + kind: { + type: 'select', + choices: [ + { value: 'minimal', label: 'Minimal' }, + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' }, + { value: 'xhigh', label: 'Extra high' }, + { value: 'max', label: 'Max' } + ], + defaultValue: 'medium' + }, + apply: { + launchArgs: (value) => ['--effort', String(value)], + agentArgsOverride: (tokens) => hasFlag(tokens, ['--effort']), + removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--effort']) + } +} + +export const CODEBUDDY_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = { + supportsWorkerLaunchPreferences: true, + // Stable CLI aliases let CodeBuddy resolve the account's current model choices. + models: [ + { id: 'default-model', label: 'Default' }, + { id: 'fast-model', label: 'Fast' }, + { id: 'balanced-model', label: 'Balanced' }, + { id: 'primary-model', label: 'Primary' }, + { id: 'deep-model', label: 'Deep' } + ].map((model) => ({ ...model, options: [CODEBUDDY_EFFORT] })), + modelApply: { + launchArgs: (value) => ['--model', String(value)], + agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']), + removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--model']) + }, + unknownModelOptions: [CODEBUDDY_EFFORT] +} diff --git a/src/shared/agent-session-option-catalog.ts b/src/shared/agent-session-option-catalog.ts index a8e26f5a0ed..74a7f384801 100644 --- a/src/shared/agent-session-option-catalog.ts +++ b/src/shared/agent-session-option-catalog.ts @@ -1,3 +1,4 @@ +import { CODEBUDDY_SESSION_OPTION_CATALOG } from './agent-session-option-catalog-codebuddy' import type { AgentType } from './agent-status-types' import { ANTIGRAVITY_SESSION_OPTION_CATALOG } from './agent-session-option-catalog-antigravity' import { @@ -34,6 +35,7 @@ export { createClaudeCatalogOptions } const CATALOGS: AgentSessionOptionCatalogMap = { antigravity: ANTIGRAVITY_SESSION_OPTION_CATALOG, claude: CLAUDE_SESSION_OPTION_CATALOG, + codebuddy: CODEBUDDY_SESSION_OPTION_CATALOG, codex: CODEX_SESSION_OPTION_CATALOG, gemini: GEMINI_SESSION_OPTION_CATALOG, cursor: CURSOR_SESSION_OPTION_CATALOG, diff --git a/src/shared/agent-session-record.ts b/src/shared/agent-session-record.ts index 2b85695a845..12a26e64ac7 100644 --- a/src/shared/agent-session-record.ts +++ b/src/shared/agent-session-record.ts @@ -87,6 +87,13 @@ export type AgentSessionDeathEvidence = { kind: 'exit-observed' | 'pid-absent' | 'identity-mismatch' detail: string observedAt: number + /** Fence of the owner (or reservation) this death is about; a fence names exactly one. Absent on + * evidence older builds wrote, which then speaks for no turn. */ + ownerFence?: number + /** The death interval's lower bound: the last time the runtime holding the owner's transport + * proved it alive. Only a probe's proof records it: absent on a surface-release exit, a failed + * start, and evidence older builds wrote. */ + lastProvenAliveAt?: number } export type AgentSessionLease = { @@ -102,6 +109,8 @@ export type AgentSessionLease = { /** Reserved before any process exists, then matched against the child's environment. */ reservedSpawnToken: string | null leaseDeadlineAt: number + /** While `ownerProcess` is set, the last time its transport holder proved it alive; parking in + * `recovering` proves nothing, so it leaves this alone. */ lastRenewedAt: number handoffOperationId: string | null journalCheckpoint: AgentSessionJournalCheckpoint | null @@ -276,13 +285,20 @@ function isAgentSessionDeathEvidence(value: unknown): value is AgentSessionDeath return false } const evidence = value as Partial + const { observedAt, lastProvenAliveAt, ownerFence } = evidence return ( (evidence.kind === 'exit-observed' || evidence.kind === 'pid-absent' || evidence.kind === 'identity-mismatch') && isBoundedString(evidence.detail, MAX_ID_LENGTH) && - Number.isSafeInteger(evidence.observedAt) && - (evidence.observedAt as number) >= 0 + typeof observedAt === 'number' && + Number.isSafeInteger(observedAt) && + observedAt >= 0 && + (ownerFence === undefined || (Number.isSafeInteger(ownerFence) && ownerFence >= 0)) && + (lastProvenAliveAt === undefined || + (Number.isSafeInteger(lastProvenAliveAt) && + lastProvenAliveAt >= 0 && + lastProvenAliveAt <= observedAt)) ) } diff --git a/src/shared/agent-session-resume.test.ts b/src/shared/agent-session-resume.test.ts index 577492ddefc..397a1a9451f 100644 --- a/src/shared/agent-session-resume.test.ts +++ b/src/shared/agent-session-resume.test.ts @@ -28,6 +28,10 @@ describe('agent session resume metadata', () => { expect(isResumableTuiAgent('kimi')).toBe(true) }) + it('treats CodeBuddy as a resumable TUI agent', () => { + expect(isResumableTuiAgent('codebuddy')).toBe(true) + }) + it.each([ ['claude', { session_id: 'claude-session' }, { key: 'session_id', id: 'claude-session' }], ['codex', { session_id: 'codex-session' }, { key: 'session_id', id: 'codex-session' }], @@ -70,6 +74,7 @@ describe('agent session resume metadata', () => { it.each([ ['claude', { key: 'session_id', id: 's1' }, ['claude', '--resume', 's1']], + ['codebuddy', { key: 'session_id', id: 's1' }, ['codebuddy', '--resume', 's1']], ['codex', { key: 'session_id', id: 's1' }, ['codex', 'resume', 's1']], ['gemini', { key: 'session_id', id: 's1' }, ['gemini', '--resume', 's1']], ['antigravity', { key: 'conversation_id', id: 's1' }, ['agy', '--conversation', 's1']], diff --git a/src/shared/agent-session-resume.ts b/src/shared/agent-session-resume.ts index 4cbc240c764..40a3699af3c 100644 --- a/src/shared/agent-session-resume.ts +++ b/src/shared/agent-session-resume.ts @@ -5,6 +5,7 @@ import type { TuiAgent } from './tui-agent' export const RESUMABLE_TUI_AGENTS = [ 'claude', + 'codebuddy', 'codex', 'qoder', 'gemini', @@ -196,6 +197,7 @@ export function extractAgentProviderSession( // since recent Claude Code names the transcript file with a UUID that differs // from the hook session_id (so the id-based glob no longer finds it). case 'qoder': + case 'codebuddy': case 'claude': case 'codex': { const id = readSessionId(payload, ['session_id']) @@ -277,6 +279,8 @@ export function getAgentResumeArgv( ): string[] | null { const id = providerSession.id switch (agent) { + case 'codebuddy': + return providerSession.key === 'session_id' ? ['codebuddy', '--resume', id] : null case 'claude': return providerSession.key === 'session_id' ? ['claude', '--resume', id] : null case 'codex': diff --git a/src/shared/agent-session-wire.ts b/src/shared/agent-session-wire.ts index 445582c4ad3..513243f8f54 100644 --- a/src/shared/agent-session-wire.ts +++ b/src/shared/agent-session-wire.ts @@ -187,6 +187,9 @@ export type AgentSessionStatusSummary = { /** With `hostExecutionOwned`: whether that child has proven its start. `starting` is a * published session whose provider has not yet answered startup; absent on older hosts. */ hostExecutionPhase?: 'starting' | 'ready' + /** The current provider child, distinct from the conversation and from replacement children. + * Absent on older hosts and whenever this host has no live child. */ + hostExecutionChild?: { generation: string | null; fence: number } latestPrompt: string /** Provider model in force for the next turn; absent until the host has read the options. */ model?: string diff --git a/src/shared/agent-type-label.ts b/src/shared/agent-type-label.ts index a4d70a76037..72d1b1b6d63 100644 --- a/src/shared/agent-type-label.ts +++ b/src/shared/agent-type-label.ts @@ -4,6 +4,7 @@ import type { AgentType } from './agent-status-types' // (e.g. native chat's empty state on both surfaces) from one source of truth. const WELL_KNOWN_LABELS: Record = { claude: 'Claude', + codebuddy: 'CodeBuddy', openclaude: 'OpenClaude', codex: 'Codex', qoder: 'Qoder CLI', diff --git a/src/shared/ai-vault-resume-command.test.ts b/src/shared/ai-vault-resume-command.test.ts index da8ced39e0e..db89cb41263 100644 --- a/src/shared/ai-vault-resume-command.test.ts +++ b/src/shared/ai-vault-resume-command.test.ts @@ -28,6 +28,17 @@ describe('buildAiVaultResumeCommand', () => { ).toBe('cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && codex resume ""session-1"""') }) + it('resumes CodeBuddy sessions with the Claude-compatible --resume flag', () => { + expect( + buildAiVaultResumeCommand({ + agent: 'codebuddy', + sessionId: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee', + cwd: '/repo/app', + platform: 'darwin' + }) + ).toBe("cd '/repo/app' && codebuddy --resume 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'") + }) + it('builds a direct queued command for a live cmd shell', () => { expect( buildAiVaultResumeCommand({ diff --git a/src/shared/ai-vault-resume-command.ts b/src/shared/ai-vault-resume-command.ts index cfff419c95c..efb2ddbe5d3 100644 --- a/src/shared/ai-vault-resume-command.ts +++ b/src/shared/ai-vault-resume-command.ts @@ -218,6 +218,7 @@ function buildAgentResumeInvocation( return `${baseCommand} resume ${sessionArg}` case 'cline': return `${baseCommand} --id ${sessionArg}` + case 'codebuddy': case 'claude': case 'zcode': case 'cursor': diff --git a/src/shared/ai-vault-session-deletion.ts b/src/shared/ai-vault-session-deletion.ts index d3c2211ab84..4875ab6e889 100644 --- a/src/shared/ai-vault-session-deletion.ts +++ b/src/shared/ai-vault-session-deletion.ts @@ -40,6 +40,7 @@ export const AI_VAULT_DELETABLE_AGENTS = [ 'pi', 'omp', 'claude', + 'codebuddy', 'rovo', 'grok', 'cline' diff --git a/src/shared/ai-vault-types.ts b/src/shared/ai-vault-types.ts index 28f93dcfbe1..e337f98d7a0 100644 --- a/src/shared/ai-vault-types.ts +++ b/src/shared/ai-vault-types.ts @@ -3,6 +3,7 @@ import type { ExecutionHostId, ExecutionHostScope } from './execution-host' export const AI_VAULT_AGENTS = [ 'claude', + 'codebuddy', 'codex', 'hermes', 'pi', @@ -51,6 +52,7 @@ export type AiVaultGroup = 'project' | 'folder' | 'agent' export const AI_VAULT_AGENT_LABELS = { claude: 'Claude', + codebuddy: 'CodeBuddy', codex: 'Codex', hermes: 'Hermes', pi: 'Pi', diff --git a/src/shared/codebuddy-agent.test.ts b/src/shared/codebuddy-agent.test.ts new file mode 100644 index 00000000000..6915cf8d573 --- /dev/null +++ b/src/shared/codebuddy-agent.test.ts @@ -0,0 +1,67 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { createHookListenerState } from './agent-hook-listener/listener-state' +import { normalizeAndAccept } from './agent-hook-listener-test-harness' +import { recognizeAgentProcessFromCommandLine } from './agent-process-recognition' +import { getAgentSessionOptionCatalog } from './agent-session-option-catalog' +import { getAgentResumeArgv } from './agent-session-resume' + +describe('CodeBuddy harness', () => { + it('replays the live question lifecycle without settling at late SessionStart', () => { + const state = createHookListenerState() + const events = readFileSync( + join(__dirname, '__fixtures__/codebuddy-question-hooks.jsonl'), + 'utf8' + ) + .trim() + .split('\n') + .map((line) => JSON.parse(line)) + const rows = events.map((event) => normalizeAndAccept(state, 'codebuddy', event)) + expect(rows.map((row) => row?.payload.state ?? null)).toEqual([ + 'working', + null, + 'waiting', + 'working', + 'working', + 'done' + ]) + expect(rows[2]?.payload).toMatchObject({ agentType: 'codebuddy', toolName: 'AskUserQuestion' }) + expect(rows[5]?.payload).toMatchObject({ agentType: 'codebuddy', lastAssistantMessage: 'Red' }) + expect(rows[5]?.providerSession?.id).toBe('codebuddy-verification') + }) + + it.each([ + 'codebuddy', + 'cbc', + 'C:\\Tools\\codebuddy.cmd', + 'node /usr/lib/node_modules/@tencent-ai/codebuddy-code/dist/codebuddy.js' + ])('recognizes interactive %s', (command) => { + expect(recognizeAgentProcessFromCommandLine(command)?.agent).toBe('codebuddy') + }) + + it('excludes print runs and resumes the original provider session', () => { + for (const flag of ['--print', '--serve', '--acp', '--bg', '--background']) { + expect(recognizeAgentProcessFromCommandLine(`codebuddy ${flag}`)).toBeNull() + } + expect(recognizeAgentProcessFromCommandLine('codebuddy -- --serve')?.agent).toBe('codebuddy') + expect(getAgentResumeArgv('codebuddy', { key: 'session_id', id: 'session-1' })).toEqual([ + 'codebuddy', + '--resume', + 'session-1' + ]) + }) + + it('passes explicit model and effort preferences through the CLI flags', () => { + const catalog = getAgentSessionOptionCatalog('codebuddy') + expect(catalog?.modelApply.launchArgs?.('fast-model')).toEqual(['--model', 'fast-model']) + expect(catalog?.unknownModelOptions?.[0].apply.launchArgs?.('high')).toEqual([ + '--effort', + 'high' + ]) + expect(catalog?.modelApply.removeAgentArgs?.(['--model', 'old', '--', 'prompt'])).toEqual([ + '--', + 'prompt' + ]) + }) +}) diff --git a/src/shared/codebuddy-headless-command.ts b/src/shared/codebuddy-headless-command.ts new file mode 100644 index 00000000000..5c632b27b3b --- /dev/null +++ b/src/shared/codebuddy-headless-command.ts @@ -0,0 +1,13 @@ +import { agentArgOptionTokens } from './agent-session-option-agent-args' +import { isPrintModeHeadlessOneShotCommand, optionName } from './print-mode-headless-command' + +const NON_INTERACTIVE_FLAGS = new Set(['--serve', '--acp', '--bg', '--background']) + +export function isCodebuddyNonInteractiveCommand(tokens: readonly string[]): boolean { + return ( + isPrintModeHeadlessOneShotCommand(tokens) || + agentArgOptionTokens(tokens) + .slice(1) + .some((token) => NON_INTERACTIVE_FLAGS.has(optionName(token))) + ) +} diff --git a/src/shared/native-chat-subagent-attribution.ts b/src/shared/native-chat-subagent-attribution.ts new file mode 100644 index 00000000000..19619ec6c87 --- /dev/null +++ b/src/shared/native-chat-subagent-attribution.ts @@ -0,0 +1,42 @@ +// Whose words a transcript row carries, when they are not the session's own agent's. +// +// A session's subagents write into its journal, and the transcript keeps their rows +// where they happened. Each such row names the subagent that wrote it, using the +// label that subagent's spawn roster already gives it, so a child's words are never +// read as its parent's. Shared so desktop and mobile name the same agent. + +import { agentJournalItemSubagentId } from './agent-session-journal-producer' +import { isSubagentGroupBlock, type NativeChatMessage } from './native-chat-types' + +/** English copy: the desktop's i18n fallbacks, and mobile's text as-is. */ +export const NATIVE_CHAT_SUBAGENT_ATTRIBUTION_COPY = { + unnamed: 'Subagent', + writtenBy: 'Written by subagent {{value0}}' +} as const + +/** Each subagent's roster label, by the id its rows carry. */ +export function nativeChatSubagentLabels( + messages: readonly NativeChatMessage[] +): ReadonlyMap { + const labels = new Map() + for (const message of messages) { + for (const block of message.blocks) { + if (isSubagentGroupBlock(block)) { + for (const agent of block.agents) { + labels.set(agent.id, agent.label) + } + } + } + } + return labels +} + +/** The roster's name for the subagent that wrote a row. Undefined for the + * session's own rows, and for a subagent no loaded roster names. */ +export function nativeChatSubagentLabel( + labels: ReadonlyMap | undefined, + message: NativeChatMessage +): string | undefined { + const agentId = agentJournalItemSubagentId(message) + return agentId === null ? undefined : labels?.get(agentId) +} diff --git a/src/shared/native-chat-tool-fold.ts b/src/shared/native-chat-tool-fold.ts index e08a722c242..f3c72a45a6b 100644 --- a/src/shared/native-chat-tool-fold.ts +++ b/src/shared/native-chat-tool-fold.ts @@ -8,6 +8,7 @@ import { type NativeChatToolCallBlock, type NativeChatToolResultBlock } from './native-chat-types' +import { agentJournalItemSubagentId } from './agent-session-journal-producer' import { isKnownHarnessInjectedUserTurnText } from './harness-injected-user-turns' import { isNoiseMessage } from './native-chat-noise' @@ -78,14 +79,23 @@ function dropUnattributableToolResults(message: NativeChatMessage): NativeChatMe return blocks.length > 0 ? { ...message, blocks } : null } -/** Fold consecutive tool-only messages into their preceding assistant turn. */ +/** Fold consecutive tool-only messages into their preceding assistant turn. + * Only into a run the same agent wrote: a subagent's calls interleave with its + * parent's in one journal, and absorbing one into the other would present one + * agent's work as the other's. */ export function foldToolMessages(messages: readonly NativeChatMessage[]): NativeChatMessage[] { const output: NativeChatMessage[] = [] let mutableAssistantIndex = -1 let clonedAssistantIndex = -1 for (const message of messages) { - if (isHarnessSidecarToolMessage(message) && mutableAssistantIndex >= 0) { - const index = mutableAssistantIndex + const foldTarget = + mutableAssistantIndex >= 0 && + agentJournalItemSubagentId(output[mutableAssistantIndex]) === + agentJournalItemSubagentId(message) + ? mutableAssistantIndex + : -1 + if (isHarnessSidecarToolMessage(message) && foldTarget >= 0) { + const index = foldTarget const assistant = output[index] if (assistant?.role === 'assistant') { if (clonedAssistantIndex !== index) { @@ -100,8 +110,8 @@ export function foldToolMessages(messages: readonly NativeChatMessage[]): Native continue } } - if (isToolOnlyMessage(message) && mutableAssistantIndex >= 0) { - const index = mutableAssistantIndex + if (isToolOnlyMessage(message) && foldTarget >= 0) { + const index = foldTarget const assistant = output[index] if (assistant?.role !== 'assistant') { output.push(message) diff --git a/src/shared/native-chat-turn-fold.test.ts b/src/shared/native-chat-turn-fold.test.ts index cd7e4de024d..d6bba89e552 100644 --- a/src/shared/native-chat-turn-fold.test.ts +++ b/src/shared/native-chat-turn-fold.test.ts @@ -147,3 +147,33 @@ describe('nativeChatTurnFold', () => { expect(foldedRows.size).toBe(0) }) }) + +describe("a subagent's words are never the turn's answer", () => { + // The reported shape: the parent replies, then a subagent it launched keeps + // narrating into the same journal after the parent's last word. + const rows: NativeChatTurnFoldRow[] = [ + row({ role: 'user' }), + row(), + row({ agentId: 'task-1' }), + row({ agentId: 'task-1', rendersProse: false }) + ] + + it("names the session's own last prose, not a subagent's later prose", () => { + expect(nativeChatTurnAnswerRows(rows).get('turn-1')).toBe(1) + }) + + it("folds the subagent's prose behind a settled turn and keeps the parent's answer", () => { + const { foldedRows } = nativeChatTurnFold({ + rows, + settledTurnKeys: SETTLED, + expandedTurnKeys: NONE + }) + expect(foldedRows.has(1)).toBe(false) + expect(foldedRows.has(2)).toBe(true) + }) + + it('reports no answer for a turn in which only subagents spoke', () => { + const onlyChildren = [row({ role: 'user' }), row({ agentId: 'task-1' })] + expect(nativeChatTurnAnswerRows(onlyChildren).has('turn-1')).toBe(false) + }) +}) diff --git a/src/shared/native-chat-turn-fold.ts b/src/shared/native-chat-turn-fold.ts index 5ceb0ef8989..52407a9d524 100644 --- a/src/shared/native-chat-turn-fold.ts +++ b/src/shared/native-chat-turn-fold.ts @@ -9,6 +9,7 @@ // Shared because desktop and mobile both draw this disclosure, and a fold that // hides a different row on each surface is the same bug twice. +import { isRootAgentJournalItem } from './agent-session-journal-producer' import type { NativeChatRole } from './native-chat-types' /** What the fold needs to know about one transcript row. Deliberately not a @@ -24,6 +25,8 @@ export type NativeChatTurnFoldRow = { * spawn roster or a background task. That row is the durable report of how * the work ended — often the only one — so it never folds. */ outlivesTurn: boolean + /** The subagent that produced the row. Absent ⇒ the session's own agent. */ + agentId?: string } export type NativeChatTurnFold = { @@ -39,14 +42,21 @@ export const NATIVE_CHAT_EMPTY_TURN_FOLD: NativeChatTurnFold = { foldableTurnKeys: new Set() } -/** The index of each turn's answer: its last assistant row that renders prose. - * A turn with no such row has no answer, and folds whole. */ +/** The index of each turn's answer: the last prose the session's own agent + * wrote. A subagent that is still narrating after its parent's last word would + * otherwise stand as the turn's only visible reply. A turn with no such row has + * no answer, and folds whole. */ export function nativeChatTurnAnswerRows( rows: readonly NativeChatTurnFoldRow[] ): ReadonlyMap { const answers = new Map() for (const [index, row] of rows.entries()) { - if (row.turnKey !== undefined && row.role === 'assistant' && row.rendersProse) { + if ( + row.turnKey !== undefined && + row.role === 'assistant' && + row.rendersProse && + isRootAgentJournalItem(row) + ) { answers.set(row.turnKey, index) } } diff --git a/src/shared/native-chat-turn-status.ts b/src/shared/native-chat-turn-status.ts index 1b0613bd716..acbab30aabf 100644 --- a/src/shared/native-chat-turn-status.ts +++ b/src/shared/native-chat-turn-status.ts @@ -4,6 +4,8 @@ // (used directly — mobile ships English only) so the two surfaces never drift. // Everything here is pure; each platform owns its own clock. +import type { NativeChatMessage } from './native-chat-types' + export const NATIVE_CHAT_TURN_STATUS_COPY = { thinking: 'Thinking', working: 'Working…', @@ -101,6 +103,23 @@ export type NativeChatTurnStatus = { export type NativeChatTurnTimingByTurn = Readonly> +/** The live turn's key when the transcript has no user message to hang it on. */ +export const NATIVE_CHAT_UNANCHORED_TURN_KEY = '__unanchored__' + +/** The user message whose bar carries the live clock: the one the host says opened + * the running turn — a message sent while it runs is not that one — else, when the + * host names none, the latest user message. */ +export function selectNativeChatActiveTurnKey( + messages: readonly NativeChatMessage[], + activeTurnOpenedBy?: string | null +): string { + return ( + activeTurnOpenedBy ?? + messages.findLast((message) => message.role === 'user')?.id ?? + NATIVE_CHAT_UNANCHORED_TURN_KEY + ) +} + /** The turn-timing state machine, lifted out of the React hook so desktop and * mobile stamp start/stop identically. Returns the same reference when nothing * changed so callers can bail out of a state update. */ diff --git a/src/shared/native-chat-types.ts b/src/shared/native-chat-types.ts index f33158d9a0f..392ebb5b8f6 100644 --- a/src/shared/native-chat-types.ts +++ b/src/shared/native-chat-types.ts @@ -13,7 +13,8 @@ import type { import type { AgentSessionFailureFact } from './agent-session-failure' import type { AgentJournalMessageSendMode, - AgentJournalPosition + AgentJournalPosition, + AgentJournalProducerLinkage } from './agent-session-journal-types' import type { AgentType } from './agent-status-types' import type { NativeChatToolMetadata } from './native-chat-tool-identity' @@ -190,7 +191,9 @@ export type NativeChatBlock = | NativeChatSubagentGroupBlock | NativeChatBackgroundTaskBlock -export type NativeChatMessage = { +/** A transcript row. Structured rows carry the journal row's producer linkage, so + * "who said this" survives the projection; terminal-backed rows carry none. */ +export type NativeChatMessage = AgentJournalProducerLinkage & { /** Stable across re-reads/appends so the assembler and the renderer list can * dedup and key by it. */ id: string diff --git a/src/shared/opencode-installed-plugin.test.ts b/src/shared/opencode-installed-plugin.test.ts new file mode 100644 index 00000000000..d7814c1229b --- /dev/null +++ b/src/shared/opencode-installed-plugin.test.ts @@ -0,0 +1,47 @@ +import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { + isInstalledOpenCodePluginCurrent, + isOverlayOpenCodePluginCurrent +} from './opencode-installed-plugin' + +describe('installed OpenCode plugin currency', () => { + let dir: string + let pluginPath: string + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'orca-opencode-installed-plugin-')) + pluginPath = join(dir, 'orca-opencode-status.js') + }) + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + }) + + it('treats a missing or different file as stale', () => { + expect(isInstalledOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false) + expect(isOverlayOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false) + writeFileSync(pluginPath, 'older plugin') + expect(isInstalledOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false) + expect(isOverlayOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false) + }) + + it('treats a regular file with the same bytes as current', () => { + writeFileSync(pluginPath, 'plugin') + expect(isInstalledOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(true) + expect(isOverlayOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(true) + }) + + it.skipIf(process.platform === 'win32')( + 'follows a symlink except in an overlay, where a link is a mirrored user entry', + () => { + const targetPath = join(dir, 'target.js') + writeFileSync(targetPath, 'plugin') + symlinkSync(targetPath, pluginPath) + expect(isInstalledOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(true) + expect(isOverlayOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false) + } + ) +}) diff --git a/src/shared/opencode-installed-plugin.ts b/src/shared/opencode-installed-plugin.ts new file mode 100644 index 00000000000..33a80f4d168 --- /dev/null +++ b/src/shared/opencode-installed-plugin.ts @@ -0,0 +1,22 @@ +import { lstatSync } from 'node:fs' +import { nodeFileContentsEqualSync } from './node-file-content-equality' + +// Why: OpenCode 2 reloads a plugin (and every plugin loaded after it) when its file's mtime changes, +// even with identical bytes, so installers must skip the write when Orca's plugin is already current. +// Follows symlinks: OpenCode 2 loads through them, so a linked file with Orca's bytes is current. +export function isInstalledOpenCodePluginCurrent(pluginPath: string, source: string): boolean { + try { + return nodeFileContentsEqualSync(pluginPath, source) + } catch { + return false + } +} + +// Overlay variant: a symlink there mirrors a user entry, never Orca's file, even when the bytes match. +export function isOverlayOpenCodePluginCurrent(pluginPath: string, source: string): boolean { + try { + return lstatSync(pluginPath).isFile() && nodeFileContentsEqualSync(pluginPath, source) + } catch { + return false + } +} diff --git a/src/shared/pane-agent-identity-inventory.test.ts b/src/shared/pane-agent-identity-inventory.test.ts index 41ca5090daf..028207cdfb9 100644 --- a/src/shared/pane-agent-identity-inventory.test.ts +++ b/src/shared/pane-agent-identity-inventory.test.ts @@ -308,7 +308,8 @@ const INVENTORY: readonly InventoryGroup[] = [ ['src/renderer/src/components/terminal-pane/agent-completion-title-observer.ts', 2], ['src/renderer/src/components/terminal-pane/pty-connection/shell-command-inference.ts', 4], ['src/renderer/src/components/terminal-pane/pty-output-title-observer.ts', 2], - ['src/shared/terminal-output-side-effects.ts', 3] + ['src/shared/terminal-output-side-effects.ts', 3], + ['src/shared/tui-agent-rest-signal.ts', 2] ] }, { diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index bd8b3f62a31..3a877a80900 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -236,6 +236,8 @@ export const AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY = 'agent-session.opencode2-resume.v1' as const export const AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY = 'agent-session.muse-resume.v1' as const export const AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY = 'agent-session.dsh-resume.v1' as const +export const AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY = + 'agent-session.codebuddy-resume.v1' as const export const AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY = 'agent-session.qoder-resume.v1' as const export const AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY = @@ -402,6 +404,7 @@ export const RUNTIME_CAPABILITIES = [ AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY, + AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY, FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY, GITHUB_MARK_PR_READY_RUNTIME_CAPABILITY, diff --git a/src/shared/rpc-contract/agent-launch-params.ts b/src/shared/rpc-contract/agent-launch-params.ts index 3fd12f52eed..3926c610708 100644 --- a/src/shared/rpc-contract/agent-launch-params.ts +++ b/src/shared/rpc-contract/agent-launch-params.ts @@ -18,6 +18,7 @@ import { isValidHostTerminalTabId } from '../terminal-tab-id' import { isTuiAgent } from '../tui-agent-config' import type { TuiAgent } from '../tui-agent' import { WorktreeCreate } from './worktree-create-params' +import { LaunchSourceParam } from './launch-source-param' import { TerminalTabIdParam } from './agent-session-params' import { SessionId } from './structured-agent-session-params' import { isStructuredAgentSessionIdFor } from '../structured-agent-session-create' @@ -78,16 +79,7 @@ export const AgentLaunchFields = z.object({ /** A start directory other than the workspace root. Terminal-only, and the host downgrades a * structured launch that carries one rather than ignoring it. */ cwd: z.string().min(1, 'Empty launch cwd').optional(), - /** - * Telemetry attribution, deliberately `z.string()` rather than the closed `launchSourceSchema`. - * - * Params are validated by the HOST, so a closed enum here is a version claim pointing the wrong - * way: a newer client naming a launch surface an older host has never heard of would have its - * whole launch refused over a label nothing reads as behaviour. Bookkeeping must not gate a user - * action, so the arm set stays open here and the host parses it leniently at the point it is - * actually used — the same `safeParse`-and-skip the PTY spawn already does. - */ - launchSource: z.string().optional(), + launchSource: LaunchSourceParam.optional(), /** * The pane a terminal launch should create, minted by a caller that places its own tabs. * diff --git a/src/shared/rpc-contract/launch-source-param.ts b/src/shared/rpc-contract/launch-source-param.ts new file mode 100644 index 00000000000..61004422483 --- /dev/null +++ b/src/shared/rpc-contract/launch-source-param.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +/** + * Which surface started an agent launch, deliberately `z.string()` rather than the closed + * `launchSourceSchema`. + * + * Params are validated by the HOST, so a closed enum here is a version claim pointing the wrong + * way: a newer client naming a launch surface an older host has never heard of would have its + * whole launch refused over a label nothing reads as behaviour. Bookkeeping must not gate a user + * action, so the arm set stays open here and the host resolves it leniently where it is used. + */ +export const LaunchSourceParam = z.string() diff --git a/src/shared/rpc-contract/worktree-create-params.ts b/src/shared/rpc-contract/worktree-create-params.ts index 7c0b1f55fad..04de055a639 100644 --- a/src/shared/rpc-contract/worktree-create-params.ts +++ b/src/shared/rpc-contract/worktree-create-params.ts @@ -5,6 +5,7 @@ import { workspaceSourceSchema } from '../telemetry-events' import { RUNTIME_NAVIGATION_TARGETS } from '../runtime-navigation' import { sleepingAgentLaunchConfigSchema } from '../workspace-session-sleeping-agents' import { isTuiAgent } from '../tui-agent-config' +import { LaunchSourceParam } from './launch-source-param' import { OptionalBoolean, OptionalFiniteNumber, @@ -110,6 +111,8 @@ export const WorktreeCreate = z // workspaces execute in a different shell than the client process. startupAgent: OptionalTuiAgent, startupPrompt: OptionalString, + // Which surface asked for the agent the host launches from `startupAgent` or `startupDraft`. + launchSource: LaunchSourceParam.optional(), // Why: task-driven mobile creates need desktop parity: the host chooses // the same default/detected agent and drafts the linked issue/PR URL into it. startupDraft: OptionalString, diff --git a/src/shared/skills-cli-agent-keys.ts b/src/shared/skills-cli-agent-keys.ts index f8d1a913172..1955bfa6d58 100644 --- a/src/shared/skills-cli-agent-keys.ts +++ b/src/shared/skills-cli-agent-keys.ts @@ -14,6 +14,7 @@ import type { TuiAgent } from './tui-agent' export const SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT = { claude: 'claude-code', 'claude-agent-teams': 'claude-code', + codebuddy: 'codebuddy', // Why: Orca states OpenClaude reads Claude-owned roots (native-chat-agent-profiles). openclaude: 'claude-code', codex: 'codex', diff --git a/src/shared/storage-capacity-error.ts b/src/shared/storage-capacity-error.ts new file mode 100644 index 00000000000..63575e7fa1c --- /dev/null +++ b/src/shared/storage-capacity-error.ts @@ -0,0 +1,25 @@ +export function storageCapacityErrorCode( + error: unknown, + seen = new Set() +): 'ENOSPC' | 'EDQUOT' | 'SQLITE_FULL' | null { + if (typeof error !== 'object' || error === null || seen.has(error)) { + return null + } + seen.add(error) + // node:sqlite reports SQLITE_FULL through its numeric SQLite result code. + if ( + 'code' in error && + error.code === 'ERR_SQLITE_ERROR' && + 'errcode' in error && + error.errcode === 13 + ) { + return 'SQLITE_FULL' + } + if ( + 'code' in error && + (error.code === 'ENOSPC' || error.code === 'EDQUOT' || error.code === 'SQLITE_FULL') + ) { + return error.code + } + return 'cause' in error ? storageCapacityErrorCode(error.cause, seen) : null +} diff --git a/src/shared/structured-agent-session-projection.test.ts b/src/shared/structured-agent-session-projection.test.ts index f26f409afd4..691872c3587 100644 --- a/src/shared/structured-agent-session-projection.test.ts +++ b/src/shared/structured-agent-session-projection.test.ts @@ -665,6 +665,18 @@ describe("producer linkage — a subagent's output never speaks for the parent", expect(prose).toContain('delegating') }) + it("keeps the child's output as the child's: the transcript message names its producer", () => { + // Rendering the child's rows is not enough; unless the message still says who + // wrote it, the transcript can only present it as the parent speaking. + const messages = projectStructuredItemsToNativeChat(items) + const byId = new Map(messages.map((message) => [message.id, message])) + expect(byId.get('child-prose')).toMatchObject({ agentId: 'task-1', producerKind: 'agent' }) + expect(byId.get('child-grep')).toMatchObject({ agentId: 'task-1' }) + // The session's own rows name no producer: absence is the claim that they are its own. + expect(byId.get('root-prose')).not.toHaveProperty('agentId') + expect(byId.get('root-task')).not.toHaveProperty('agentId') + }) + it("falls back to nothing rather than a child's line when the parent said nothing", () => { const summary = projectStructuredAgentSessionStatusSummary([ userAsk, diff --git a/src/shared/structured-agent-session-projection.ts b/src/shared/structured-agent-session-projection.ts index f444af0a060..f35af81b932 100644 --- a/src/shared/structured-agent-session-projection.ts +++ b/src/shared/structured-agent-session-projection.ts @@ -10,7 +10,7 @@ import { type AgentJournalSubmission, type AgentJournalTurnOutcome } from './agent-session-journal-types' -import { isRootAgentJournalItem } from './agent-session-journal-producer' +import { agentJournalLinkageFields, isRootAgentJournalItem } from './agent-session-journal-producer' import { structuredAgentSessionStatusBlock } from './structured-agent-session-status-block' import { agentJournalItemRowOrigin } from './agent-session-journal-position' import { @@ -133,8 +133,8 @@ function isAgentJournalMessageSendMode(value: string): value is AgentJournalMess const projectedItems = new WeakMap() /** Deliberately NOT scoped by producer: the transcript shows every agent's - * output. The line this module draws is that the transcript renders every item, - * while every "what is this agent doing right now" scan renders only the + * output, and each message keeps its row's linkage so the transcript can say + * whose it is. Every "what is this agent doing right now" scan renders only the * session's own agent's. */ export function projectStructuredItemsToNativeChat( items: readonly AgentJournalRenderItem[] @@ -162,6 +162,7 @@ export function projectStructuredItemToNativeChat( const message: NativeChatMessage | null = projected ? { ...agentJournalItemRowOrigin(item), + ...agentJournalLinkageFields(item), role: projected.role, blocks: projected.blocks, // A send mode this build cannot name renders as an ordinary message. diff --git a/src/shared/structured-agent-session-turn-bars.test.ts b/src/shared/structured-agent-session-turn-bars.test.ts new file mode 100644 index 00000000000..0f4732a3aac --- /dev/null +++ b/src/shared/structured-agent-session-turn-bars.test.ts @@ -0,0 +1,401 @@ +import { describe, expect, it } from 'vitest' +import type { + AgentJournalRenderItem, + AgentJournalSubmission, + AgentJournalTurnLifecycle +} from './agent-session-journal-types' +import type { NativeChatMessage } from './native-chat-types' +import { + reduceNativeChatTurnTiming, + selectNativeChatActiveTurnKey, + selectNativeChatTurnStatuses, + type NativeChatTurnTimingByTurn +} from './native-chat-turn-status' +import { + selectStructuredAgentSettledTurns, + selectStructuredAgentTurnBars, + structuredAgentTurnLocalStartedAt, + structuredAgentTurnOrigin +} from './structured-agent-session-turn-timing' + +// Host clock of a live Claude repro (journal rows 44-79): prompt A runs a ~60 s tool +// chain; 4 s later the user sends B, which Claude picks up by interrupting A's turn. +const T = 1_790_580_000_000 +const PRIOR = { requestedAt: 1_790_579_265_055, startedAt: 1_790_579_270_679 } +const PRIOR_END = 1_790_579_272_584 +const A = { sent: T + 74_595, started: T + 76_146, ended: T + 92_372 } +const B = { sent: T + 79_006, started: T + 92_372, ended: T + 144_349 } +const SESSION = 'claude:55368cfb' + +function user( + sequence: number, + clientMessageId: string, + observedAt: number +): AgentJournalRenderItem { + return { + itemId: `orca:${clientMessageId}`, + revision: 0, + sequence, + observedAt, + body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: clientMessageId }] } + } +} + +function turn( + sequence: number, + observedAt: number, + record: AgentJournalTurnLifecycle +): AgentJournalRenderItem { + return { + itemId: `legacy:claude:55368cfb:turn-lifecycle%3A${record.turnId}`, + revision: 1, + sequence, + observedAt, + body: { kind: 'turn', ...record } + } +} + +function tool(sequence: number, observedAt: number): AgentJournalRenderItem { + return { + itemId: `orca:claude-tool%3A${sequence}`, + revision: 1, + sequence, + observedAt, + body: { kind: 'tool-call', name: 'Bash', input: { command: 'sleep 15' }, state: 'running' } + } +} + +function accepted(clientMessageId: string, providerItemId: string | null): AgentJournalSubmission { + return { + clientMessageId, + fence: 1, + payloadFingerprint: 'fp', + dispatchState: providerItemId ? 'accepted' : 'pending', + providerItemId, + reason: null, + submittedAt: 1, + resolvedAt: providerItemId ? 2 : null + } +} + +const priorTurn = turn(47, PRIOR_END + 153, { + turnId: '5901cefd', + state: 'completed', + ...PRIOR, + completedAt: PRIOR_END, + durationMs: 6_218, + userItemId: `${SESSION}:5901cefd` +}) +const turn1 = { turnId: '1020ab78', userItemId: `${SESSION}:1020ab78` } +const turn2 = { turnId: '6891a0bc', userItemId: `${SESSION}:6891a0bc` } +const history = [user(44, 'prior', PRIOR.requestedAt), priorTurn] +const priorSubmission = accepted('prior', `${SESSION}:5901cefd`) + +/** B sent while turn 1 still runs (rows 54-61). */ +const whileQueued = { + turnId: turn1.turnId, + items: [ + ...history, + user(54, 'A', A.sent), + turn(57, T + 77_224, { + ...turn1, + state: 'running', + startedAt: A.started, + requestedAt: A.sent + }), + tool(58, T + 77_223), + user(60, 'B', B.sent) + ], + submissions: [priorSubmission, accepted('A', turn1.userItemId), accepted('B', null)] +} + +/** Claude ended turn 1 to pick up B (rows 62-65). */ +const turn1Ended = turn(57, A.ended + 1, { + ...turn1, + state: 'interrupted', + startedAt: A.started, + requestedAt: A.sent, + completedAt: A.ended +}) +const whileB = { + turnId: turn2.turnId, + items: [ + ...history, + user(54, 'A', A.sent), + turn1Ended, + tool(58, T + 92_329), + user(60, 'B', B.sent), + turn(65, B.started, { ...turn2, state: 'running', startedAt: B.started, requestedAt: B.sent }) + ], + submissions: [priorSubmission, accepted('A', turn1.userItemId), accepted('B', turn2.userItemId)] +} + +/** Both settled (row 78). Claude's own duration for B counts from turn 1's start. */ +const settled = { + items: [ + ...whileB.items.slice(0, -1), + turn(65, B.ended + 1, { + ...turn2, + state: 'completed', + startedAt: B.started, + requestedAt: B.sent, + completedAt: B.ended, + durationMs: 69_595 + }) + ], + submissions: whileB.submissions +} + +function message(id: string, role: NativeChatMessage['role'] = 'user'): NativeChatMessage { + return { id, role, blocks: [{ type: 'text', text: id }], timestamp: null, source: 'transcript' } +} + +describe('a send queued behind a running Claude turn', () => { + it('leaves the live bar with the prompt that opened the running turn', () => { + const bars = selectStructuredAgentTurnBars( + whileQueued.items, + whileQueued.submissions, + whileQueued.turnId + ) + expect(bars.activeTurnOpenedBy).toBe('orca:A') + const transcript = [message('orca:prior'), message('orca:A'), message('orca:B')] + expect(selectNativeChatActiveTurnKey(transcript, bars.activeTurnOpenedBy)).toBe('orca:A') + // A was sent into an idle session: it counts from its own send, as before. + expect(bars.runningTiming).toEqual({ + state: 'running', + startedAt: A.started, + requestedAt: A.sent, + observedAt: T + 77_224 + }) + expect(bars.settledTurns.has('orca:B')).toBe(false) + }) + + it("moves the bar to B when its own turn opens, counting from A's end", () => { + const bars = selectStructuredAgentTurnBars(whileB.items, whileB.submissions, whileB.turnId) + expect(bars.activeTurnOpenedBy).toBe('orca:B') + expect(bars.settledTurns.get('orca:A')).toEqual({ startedAt: A.started, workedSeconds: 17 }) + expect(bars.runningTiming && structuredAgentTurnOrigin(bars.runningTiming)).toBe(A.ended) + // A client attaching 3 s into turn 2 counts 3 s, not the 13 s B waited behind turn 1. + expect(structuredAgentTurnLocalStartedAt(bars.runningTiming!, 50_000, B.started + 3_000)).toBe( + 47_000 + ) + }) + + it('settles from the same origin the live counter used, so the bars sum to wall time', () => { + const settledTurns = selectStructuredAgentSettledTurns(settled.items, settled.submissions) + expect(settledTurns.get('orca:A')).toEqual({ startedAt: A.started, workedSeconds: 17 }) + // Not 65 s from B's send, and not Claude's own 69.6 s, which counts from turn 1's start. + expect(settledTurns.get('orca:B')).toEqual({ startedAt: B.started, workedSeconds: 51 }) + // The turn before A never overlapped a send; its duration is unchanged. + expect(settledTurns.get('orca:prior')).toEqual({ startedAt: PRIOR.startedAt, workedSeconds: 7 }) + + const liveOriginB = structuredAgentTurnOrigin( + selectStructuredAgentTurnBars(whileB.items, whileB.submissions, whileB.turnId).runningTiming! + ) + expect(Math.floor((B.ended - liveOriginB) / 1000)).toBe(51) + expect(A.ended - A.sent + (B.ended - liveOriginB)).toBe(B.ended - A.sent) + expect(selectStructuredAgentTurnBars(settled.items, settled.submissions, null)).toMatchObject({ + activeTurnOpenedBy: null, + runningTiming: null + }) + }) + + it('shows one live bar under A while B waits, then A settled and B live', () => { + const transcript = [message('orca:A'), message('orca:tool', 'assistant'), message('orca:B')] + const validTurnKeys = new Set(['orca:A', 'orca:B']) + let timing: NativeChatTurnTimingByTurn = {} + const pass = ( + snapshot: { items: AgentJournalRenderItem[]; submissions: AgentJournalSubmission[] }, + turnId: string, + now: number + ) => { + const bars = selectStructuredAgentTurnBars(snapshot.items, snapshot.submissions, turnId) + const activeTurnKey = selectNativeChatActiveTurnKey(transcript, bars.activeTurnOpenedBy) + const workingStartedAt = structuredAgentTurnLocalStartedAt(bars.runningTiming!, now, now) + timing = reduceNativeChatTurnTiming(timing, { + activeTurnKey, + validTurnKeys, + isWorking: true, + workingStartedAt, + now + }) + return selectNativeChatTurnStatuses(timing, { + activeTurnKey, + isWorking: true, + workingStartedAt, + thinking: false, + settledByTurn: bars.settledTurns + }) + } + + const queued = pass(whileQueued, whileQueued.turnId, B.sent + 1_000) + expect(timing['orca:A']?.startedAt).toBe(A.sent) + expect(timing['orca:B']).toBeUndefined() + expect(queued.completedByTurn['orca:B']).toBeUndefined() + + const running = pass(whileB, whileB.turnId, B.started + 1_000) + expect(running.completedByTurn['orca:A']?.workedSeconds).toBe(17) + expect(running.active?.startedAt).toBe(A.ended) + }) +}) + +describe('a send Codex folds into the running turn', () => { + const key = 'codex:thread:t1:0' + const items: AgentJournalRenderItem[] = [ + { ...user(1, 'first', 1_000) }, + { + itemId: 'legacy:codex:s:turn-lifecycle%3At1', + revision: 1, + sequence: 2, + observedAt: 1_200, + body: { + kind: 'turn', + turnId: 't1', + state: 'running', + userItemId: key, + requestedAt: 1_000, + startedAt: 1_200 + } + }, + { ...user(3, 'second', 4_000) } + ] + const submissions = [accepted('first', key), accepted('second', key)] + const ended: AgentJournalRenderItem[] = [ + items[0]!, + { + ...items[1]!, + observedAt: 9_000, + body: { + kind: 'turn', + turnId: 't1', + state: 'completed', + userItemId: key, + requestedAt: 1_000, + startedAt: 1_200, + completedAt: 9_000 + } + }, + items[2]! + ] + const transcript = [message('orca:first'), message('orca:second')] + + it('keeps exactly one bar, under the opening prompt, live and settled', () => { + const live = selectStructuredAgentTurnBars(items, submissions, 't1') + expect(selectNativeChatActiveTurnKey(transcript, live.activeTurnOpenedBy)).toBe('orca:first') + expect(live.runningTiming && structuredAgentTurnOrigin(live.runningTiming)).toBe(1_000) + + let timing: NativeChatTurnTimingByTurn = {} + const validTurnKeys = new Set(['orca:first', 'orca:second']) + timing = reduceNativeChatTurnTiming(timing, { + activeTurnKey: 'orca:first', + validTurnKeys, + isWorking: true, + workingStartedAt: 1_000, + now: 4_500 + }) + const after = selectStructuredAgentTurnBars(ended, submissions, null) + // The turn is over: nothing names an owner, so the key falls back to the newest prompt, + // which never ran a turn of its own and so has nothing to settle. + const activeTurnKey = selectNativeChatActiveTurnKey(transcript, after.activeTurnOpenedBy) + expect(activeTurnKey).toBe('orca:second') + timing = reduceNativeChatTurnTiming(timing, { + activeTurnKey, + validTurnKeys, + isWorking: false, + workingStartedAt: null, + now: 9_500 + }) + const statuses = selectNativeChatTurnStatuses(timing, { + activeTurnKey, + isWorking: false, + thinking: false, + settledByTurn: after.settledTurns + }) + expect(statuses.active).toBeNull() + expect(Object.keys(statuses.completedByTurn)).toEqual(['orca:first']) + expect(statuses.completedByTurn['orca:first']?.workedSeconds).toBe(8) + }) +}) + +describe('turns whose host names no opener', () => { + it('falls back to the newest prompt for an older host row with no user key', () => { + const items = [ + user(1, 'u1', 1_000), + turn(2, 1_100, { turnId: 't1', state: 'running', startedAt: 1_100 }), + user(3, 'u2', 2_000) + ] + const bars = selectStructuredAgentTurnBars(items, [], 't1') + expect(bars.activeTurnOpenedBy).toBeNull() + expect(selectNativeChatActiveTurnKey([message('orca:u1'), message('orca:u2')], null)).toBe( + 'orca:u2' + ) + // Settled timing keeps its journal-order attribution. + expect([...selectStructuredAgentSettledTurns(items).keys()]).toEqual(['orca:u1']) + }) + + it('falls back for a turn the provider opened and keyed to itself', () => { + const self = 'legacy:claude:55368cfb:turn-lifecycle%3Aresumed' + const items = [ + user(1, 'u1', 1_000), + turn(2, 5_000, { + turnId: 'resumed', + state: 'running', + startedAt: 5_000, + userItemId: self + }) + ] + expect(selectStructuredAgentTurnBars(items, [], 'resumed').activeTurnOpenedBy).toBeNull() + }) + + it('names nothing for the unanchored transcript', () => { + expect(selectNativeChatActiveTurnKey([message('a', 'assistant')], null)).toBe('__unanchored__') + }) +}) + +describe('the previous turn has no recorded end', () => { + it('leaves an unverifiable turn null and a later send counting from itself', () => { + const items = [ + user(1, 'u1', 1_000), + turn(2, 3_000, { turnId: 't1', state: 'unverifiable', userItemId: 'orca:u1' }), + user(3, 'u2', 10_000), + turn(4, 12_000, { + turnId: 't2', + state: 'completed', + userItemId: 'orca:u2', + requestedAt: 10_000, + startedAt: 11_000, + completedAt: 20_000 + }) + ] + const settledTurns = selectStructuredAgentSettledTurns(items) + expect(settledTurns.get('orca:u1')).toBeNull() + expect(settledTurns.get('orca:u2')).toEqual({ startedAt: 11_000, workedSeconds: 10 }) + }) + + it("waits out the previous row's last host revision, never past its own start", () => { + const queued = (lastSeenAt: number) => + selectStructuredAgentTurnBars( + [ + user(1, 'u1', 1_000), + turn(2, lastSeenAt, { + turnId: 't1', + state: 'unverifiable', + userItemId: 'orca:u1', + startedAt: 1_000 + }), + user(3, 'u2', 2_000), + turn(4, 9_000, { + turnId: 't2', + state: 'running', + userItemId: 'orca:u2', + requestedAt: 2_000, + startedAt: 9_000 + }) + ], + [], + 't2' + ).runningTiming! + expect(structuredAgentTurnOrigin(queued(6_000))).toBe(6_000) + expect(structuredAgentTurnOrigin(queued(12_000))).toBe(9_000) + }) +}) diff --git a/src/shared/structured-agent-session-turn-timing.ts b/src/shared/structured-agent-session-turn-timing.ts index 3f480013886..9d93639e181 100644 --- a/src/shared/structured-agent-session-turn-timing.ts +++ b/src/shared/structured-agent-session-turn-timing.ts @@ -23,12 +23,18 @@ export type StructuredAgentTurnTiming = { completedAt?: number /** The provider's own measurement; used when exact host endpoints are unavailable. */ durationMs?: number + /** Host clock this turn's send stopped waiting behind the journal's previous + * turn; present only when the send was issued before that turn ended. */ + queuedUntil?: number /** Host clock when the lifecycle row was appended; with `startedAt` it gives * the host-side lag a client must subtract to anchor a live counter. */ observedAt: number } -function readTiming(item: AgentJournalRenderItem): StructuredAgentTurnTiming | null { +function readTiming( + item: AgentJournalRenderItem, + precedingTurnEndedAt: number | undefined +): StructuredAgentTurnTiming | null { const turn = readAgentJournalTurn(item.body) if (!turn) { return null @@ -49,26 +55,47 @@ function readTiming(item: AgentJournalRenderItem): StructuredAgentTurnTiming | n durationMs !== undefined && Number.isFinite(durationMs) && durationMs >= 0 ? durationMs : undefined + // A send queued behind the previous turn counts from that turn's end (recorded, else its row's + // last host revision), never past this turn's own start: the provider opens it only after. + const queuedUntil = + precedingTurnEndedAt === undefined ? undefined : Math.min(precedingTurnEndedAt, startedAt) return { state, startedAt, ...(requested !== undefined ? { requestedAt: requested } : {}), ...(end !== undefined ? { completedAt: end } : {}), ...(measured !== undefined ? { durationMs: measured } : {}), + ...(requested !== undefined && queuedUntil !== undefined && queuedUntil > requested + ? { queuedUntil } + : {}), observedAt: item.observedAt } } -/** Timing keyed by the user message that opened each turn. A row can name the - * submission directly or by a provider key that resolves through its alias. - * Rows from older hosts carry no key and fall back +/** One turn record, read in journal order. */ +type StructuredAgentJournalTurn = { + timing: StructuredAgentTurnTiming | null + /** The user message the host names as the turn's opener; null when it names + * none (an older host, or a turn the provider opened and keyed to itself). */ + openedBy: string | null +} + +type StructuredAgentJournalTurns = { + /** Timing keyed by the user message that opened each turn. */ + byUserItem: ReadonlyMap + byTurnId: ReadonlyMap +} + +/** Every turn record in one pass over the journal. A row can name its user + * message directly or by a provider key that resolves through its alias. + * Rows from older hosts carry no key and fall back, for timing only, * to the nearest user message before them in journal order — the submission * row is written ahead of dispatch, so it always precedes the provider's * turn-start. Untimed rows are skipped unless explicitly unverifiable (null). */ -export function selectStructuredAgentTurnTimings( +function readStructuredAgentJournalTurns( items: readonly AgentJournalRenderItem[], submissions: readonly AgentJournalSubmission[] = [] -): ReadonlyMap { +): StructuredAgentJournalTurns { const itemIds = new Set(items.map((item) => item.itemId)) const aliases = new Map() // Codex folds a send issued mid-turn into the running turn under the SAME provider @@ -78,26 +105,43 @@ export function selectStructuredAgentTurnTimings( aliases.set(submission.providerItemId, agentJournalSubmissionKey(submission.clientMessageId)) } } - const timings = new Map() + const byUserItem = new Map() + const byTurnId = new Map() let precedingUserItemId: string | null = null + let precedingTurnEndedAt: number | undefined for (const item of items) { if (item.body.kind === 'message' && item.body.role === 'user') { precedingUserItemId = item.itemId continue } const turn = readAgentJournalTurn(item.body) - const timing = readTiming(item) - if (!timing && turn?.state !== 'unverifiable') { + if (!turn) { continue } - const key = turn?.userItemId - const userItemId = - key === undefined ? precedingUserItemId : itemIds.has(key) ? key : (aliases.get(key) ?? null) + const timing = readTiming(item, precedingTurnEndedAt) + precedingTurnEndedAt = timing?.completedAt ?? item.observedAt + const key = turn.userItemId + const named = key === undefined ? null : itemIds.has(key) ? key : (aliases.get(key) ?? null) + byTurnId.set(turn.turnId, { + timing, + openedBy: named === item.itemId ? null : named + }) + if (!timing && turn.state !== 'unverifiable') { + continue + } + const userItemId = key === undefined ? precedingUserItemId : named if (userItemId !== null) { - timings.set(userItemId, timing) + byUserItem.set(userItemId, timing) } } - return timings + return { byUserItem, byTurnId } +} + +export function selectStructuredAgentTurnTimings( + items: readonly AgentJournalRenderItem[], + submissions: readonly AgentJournalSubmission[] = [] +): ReadonlyMap { + return readStructuredAgentJournalTurns(items, submissions).byUserItem } /** The live turn's lifecycle timing, or null when its row carries no host start @@ -106,20 +150,15 @@ export function selectStructuredAgentRunningTurnTiming( items: readonly AgentJournalRenderItem[], turnId: string ): StructuredAgentTurnTiming | null { - for (let index = items.length - 1; index >= 0; index -= 1) { - const item = items[index] - if (item && readAgentJournalTurn(item.body)?.turnId === turnId) { - return readTiming(item) - } - } - return null + return readStructuredAgentJournalTurns(items).byTurnId.get(turnId)?.timing ?? null } /** The single instant every reading of a turn's elapsed time counts from: the - * send that opened it when the host named one, the provider turn-open otherwise. + * send that opened it when the host named one — or, for a send queued behind the + * previous turn, that turn's end — and the provider turn-open otherwise. * One origin is what keeps the live counter and the settled duration agreeing. */ export function structuredAgentTurnOrigin(timing: StructuredAgentTurnTiming): number { - return timing.requestedAt ?? timing.startedAt + return timing.queuedUntil ?? timing.requestedAt ?? timing.startedAt } /** Whole seconds a settled turn ran, or null when the host never observed its end. */ @@ -129,9 +168,9 @@ export function completedStructuredAgentTurnSeconds( if (!timing || (timing.state !== 'completed' && timing.state !== 'interrupted')) { return null } - // Provider durations may begin at turn-open, so exact host endpoints preserve the live origin. + // Provider durations may begin before the live origin (turn-open, or a turn this send queued behind). if (timing.requestedAt !== undefined && timing.completedAt !== undefined) { - return Math.max(0, Math.floor((timing.completedAt - timing.requestedAt) / 1000)) + return Math.max(0, Math.floor((timing.completedAt - structuredAgentTurnOrigin(timing)) / 1000)) } if (timing.durationMs !== undefined) { return Math.floor(timing.durationMs / 1000) @@ -170,9 +209,16 @@ export function structuredAgentTurnLocalStartedAt( export function selectStructuredAgentSettledTurns( items: readonly AgentJournalRenderItem[], submissions: readonly AgentJournalSubmission[] = [] +): NativeChatSettledTurns { + return settledTurnsOf(readStructuredAgentJournalTurns(items, submissions), submissions) +} + +function settledTurnsOf( + turns: StructuredAgentJournalTurns, + submissions: readonly AgentJournalSubmission[] ): NativeChatSettledTurns { const settled = new Map() - for (const [userItemId, timing] of selectStructuredAgentTurnTimings(items, submissions)) { + for (const [userItemId, timing] of turns.byUserItem) { const workedSeconds = completedStructuredAgentTurnSeconds(timing) settled.set( userItemId, @@ -189,3 +235,24 @@ export function selectStructuredAgentSettledTurns( } return settled } + +/** Everything a chat surface reads off the journal for its turn bars, from one + * pass: settled durations, the running turn's timing, and the user message that + * owns its bar — which a message sent while the turn runs is not. */ +export function selectStructuredAgentTurnBars( + items: readonly AgentJournalRenderItem[], + submissions: readonly AgentJournalSubmission[], + turnId: string | null +): { + settledTurns: NativeChatSettledTurns + runningTiming: StructuredAgentTurnTiming | null + activeTurnOpenedBy: string | null +} { + const turns = readStructuredAgentJournalTurns(items, submissions) + const running = turnId === null ? undefined : turns.byTurnId.get(turnId) + return { + settledTurns: settledTurnsOf(turns, submissions), + runningTiming: running?.timing ?? null, + activeTurnOpenedBy: running?.openedBy ?? null + } +} diff --git a/src/shared/telemetry-property-schemas.ts b/src/shared/telemetry-property-schemas.ts index cee76b677d6..a45a2f8d9f2 100644 --- a/src/shared/telemetry-property-schemas.ts +++ b/src/shared/telemetry-property-schemas.ts @@ -10,6 +10,7 @@ import type { GlobalSettings } from './global-settings-types' export const AGENT_KIND_VALUES = [ 'claude-code', 'claude-agent-teams', + 'codebuddy', 'openclaude', 'codex', 'autohand', @@ -140,6 +141,10 @@ export const launchSourceSchema = z.enum([ 'conflict_resolution', 'source_control_recovery', 'terminal_context_menu', + // Launches the host performs for a caller outside the desktop app. + 'cli', + 'mobile', + 'orchestration', 'unknown' ]) export type LaunchSource = z.infer diff --git a/src/shared/terminal-osc133-command-finished.ts b/src/shared/terminal-osc133-command-finished.ts index 9468e1aaa5f..f0a9831d029 100644 --- a/src/shared/terminal-osc133-command-finished.ts +++ b/src/shared/terminal-osc133-command-finished.ts @@ -59,15 +59,19 @@ export type Osc133CommandFinishedScanner = { export function createOsc133CommandFinishedScanner( onCommandFinished: (bestEffortExitCode: number | null) => void, - /** OSC 133;C — the shell exec'd a command; the pane's foreground changed. */ - onCommandStarted?: () => void + /** + * OSC 133;C — the shell exec'd a command; the pane's foreground changed. + * `endInChunk` is the index in the scanned chunk just past the sequence, where the command's own + * output begins. + */ + onCommandStarted?: (endInChunk: number) => void ): Osc133CommandFinishedScanner { let carry = '' - const handleOsc133 = (payload: string): void => { + const handleOsc133 = (payload: string, endInChunk: number): void => { const [sequence, exitCode] = payload.split(';') if (sequence === 'C') { - onCommandStarted?.() + onCommandStarted?.(endInChunk) return } if (sequence === 'D') { @@ -77,6 +81,8 @@ export function createOsc133CommandFinishedScanner( const scan = (data: string): void => { let combined = carry + data + // `combined` starts at this index in `data`; negative while it still holds the carry. + let combinedStartInChunk = -carry.length carry = '' while (combined.length > 0) { @@ -97,8 +103,13 @@ export function createOsc133CommandFinishedScanner( return } - handleOsc133(combined.slice(payloadStart, terminator.index)) - combined = combined.slice(terminator.index + terminator.length) + const sequenceEnd = terminator.index + terminator.length + handleOsc133( + combined.slice(payloadStart, terminator.index), + combinedStartInChunk + sequenceEnd + ) + combined = combined.slice(sequenceEnd) + combinedStartInChunk += sequenceEnd } } diff --git a/src/shared/terminal-session-state-save-failure.test.ts b/src/shared/terminal-session-state-save-failure.test.ts new file mode 100644 index 00000000000..aee76908a62 --- /dev/null +++ b/src/shared/terminal-session-state-save-failure.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { + createTerminalSessionStateSaveFailureMessage, + isTerminalSessionStateSaveFailure, + isTerminalSessionStorageCapacityFailure +} from './terminal-session-state-save-failure' + +describe('terminal save failure classification', () => { + it.each(['ENOSPC', 'EDQUOT', 'SQLITE_FULL'])('recognizes %s through a wrapper', (code) => { + const message = createTerminalSessionStateSaveFailureMessage( + new Error('write failed', { cause: Object.assign(new Error('failure'), { code }) }) + ) + expect(isTerminalSessionStorageCapacityFailure(message)).toBe(true) + expect(isTerminalSessionStateSaveFailure(message)).toBe(true) + }) + + it.each(['EACCES', 'EPERM', 'EIO', 'SQLITE_BUSY', 'profile-state-writer-exit'])( + 'does not diagnose %s as exhausted storage', + (code) => { + const message = createTerminalSessionStateSaveFailureMessage({ code, message: 'disk full' }) + expect(isTerminalSessionStateSaveFailure(message)).toBe(true) + expect(isTerminalSessionStorageCapacityFailure(message)).toBe(false) + } + ) + + it('treats legacy and unknown host errors as unclassified', () => { + for (const message of [ + 'ORCA_TERMINAL_SESSION_STATE_SAVE_FAILED: local storage is unavailable.', + 'Failed to save terminal session state', + createTerminalSessionStateSaveFailureMessage(new Error('disk full')) + ]) { + expect(isTerminalSessionStateSaveFailure(message)).toBe(true) + expect(isTerminalSessionStorageCapacityFailure(message)).toBe(false) + } + }) + + it('handles cyclic causes', () => { + const error = new Error('cycle') + error.cause = error + expect( + isTerminalSessionStorageCapacityFailure(createTerminalSessionStateSaveFailureMessage(error)) + ).toBe(false) + }) +}) diff --git a/src/shared/terminal-session-state-save-failure.ts b/src/shared/terminal-session-state-save-failure.ts index 309a98b653a..c286cd568bd 100644 --- a/src/shared/terminal-session-state-save-failure.ts +++ b/src/shared/terminal-session-state-save-failure.ts @@ -1,10 +1,18 @@ +import { storageCapacityErrorCode } from './storage-capacity-error' + export const TERMINAL_SESSION_STATE_SAVE_FAILED_CODE = 'ORCA_TERMINAL_SESSION_STATE_SAVE_FAILED' export const TERMINAL_SESSION_STATE_SAVE_FAILED_MESSAGE = - 'Orca could not save this terminal session because local storage is unavailable.' + 'Orca could not save this terminal session.' +const CAPACITY_MARKER = '[storage-capacity-exhausted]' -export function createTerminalSessionStateSaveFailureMessage(): string { - return `${TERMINAL_SESSION_STATE_SAVE_FAILED_CODE}: ${TERMINAL_SESSION_STATE_SAVE_FAILED_MESSAGE}` +export function createTerminalSessionStateSaveFailureMessage(error?: unknown): string { + const capacity = storageCapacityErrorCode(error) ? ` ${CAPACITY_MARKER}` : '' + return `${TERMINAL_SESSION_STATE_SAVE_FAILED_CODE}: ${TERMINAL_SESSION_STATE_SAVE_FAILED_MESSAGE}${capacity}` +} + +export function isTerminalSessionStorageCapacityFailure(message: string): boolean { + return isTerminalSessionStateSaveFailure(message) && message.includes(CAPACITY_MARKER) } export function isTerminalSessionStateSaveFailure(message: string): boolean { diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts index 15318913968..27cda1d1979 100644 --- a/src/shared/tui-agent-config.ts +++ b/src/shared/tui-agent-config.ts @@ -97,6 +97,11 @@ const TUI_AGENT_CONFIG_SOURCE: Record = { promptInjectionMode: 'stdin-after-start', pasteNeedsTypedRequest: true }, + codebuddy: { + detectCmd: 'codebuddy', + detectCmdAliases: ['cbc'], + promptInjectionMode: 'argv' + }, openclaude: { detectCmd: 'openclaude', promptInjectionMode: 'argv', @@ -148,7 +153,6 @@ const TUI_AGENT_CONFIG_SOURCE: Record = { detectCmd: 'opencode2', // The private server inherits this pane's hook endpoint and identity. launchCmd: 'opencode2 --standalone', - expectedProcess: 'opencode2', promptInjectionMode: 'flag-prompt', draftPasteReadySignal: 'render-cursor-after-bracketed-paste', draftPasteReadyTimeoutMs: 20_000 diff --git a/src/shared/tui-agent-display-names.ts b/src/shared/tui-agent-display-names.ts index a90daa3242f..31b6035a953 100644 --- a/src/shared/tui-agent-display-names.ts +++ b/src/shared/tui-agent-display-names.ts @@ -8,6 +8,7 @@ import type { TuiAgent } from './tui-agent' export const TUI_AGENT_DISPLAY_NAMES: Record = { claude: 'Claude', 'claude-agent-teams': 'Claude Agent Teams', + codebuddy: 'CodeBuddy', openclaude: 'OpenClaude', codex: 'Codex', devin: 'Devin', diff --git a/src/shared/tui-agent-permissions.ts b/src/shared/tui-agent-permissions.ts index 3a9912bf066..9fc8eb0a691 100644 --- a/src/shared/tui-agent-permissions.ts +++ b/src/shared/tui-agent-permissions.ts @@ -5,6 +5,7 @@ export type AgentPermissionMode = 'yolo' | 'manual' | 'mixed' export const YOLO_TUI_AGENT_ARGS: Partial> = { claude: '--dangerously-skip-permissions', + codebuddy: '--dangerously-skip-permissions', 'claude-agent-teams': '--dangerously-skip-permissions', openclaude: '--dangerously-skip-permissions', codex: '--dangerously-bypass-approvals-and-sandbox', diff --git a/src/shared/tui-agent-rest-signal.test.ts b/src/shared/tui-agent-rest-signal.test.ts new file mode 100644 index 00000000000..6e573acc0b0 --- /dev/null +++ b/src/shared/tui-agent-rest-signal.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest' +import type { TuiAgent } from './tui-agent' +import { isTuiAgent, TUI_AGENT_CONFIG } from './tui-agent-config' +import { getTuiAgentRestSignal, type TuiAgentRestSignal } from './tui-agent-rest-signal' + +// Why a full Record: adding a TuiAgent fails to compile here until someone decides what its +// rest signal is, and a title-table change that moves an agent shows up in review. +const EXPECTED_REST_SIGNALS: Record = { + dsh: 'hook-done', + codex: 'synthetic-title', + cursor: 'synthetic-title', + pi: 'synthetic-title', + omp: 'synthetic-title', + droid: 'synthetic-title', + hermes: 'synthetic-title', + devin: 'synthetic-title', + zcode: 'synthetic-title', + claude: 'title', + 'claude-agent-teams': 'title', + openclaude: 'title', + opencode: 'title', + opencode2: 'title', + 'mimo-code': 'title', + gemini: 'title', + antigravity: 'title', + aider: 'title', + openclaw: 'title', + copilot: 'title', + grok: 'title', + muse: 'ready-body', + qoder: 'ready-body', + codebuddy: 'none', + autohand: 'none', + ante: 'none', + trae: 'none', + 'prime-agent': 'none', + goose: 'none', + amp: 'none', + kilo: 'none', + kiro: 'none', + crush: 'none', + aug: 'none', + cline: 'none', + codebuff: 'none', + freebuff: 'none', + 'command-code': 'none', + continue: 'none', + kimi: 'none', + 'mistral-vibe': 'none', + 'qwen-code': 'none', + rovo: 'none' +} + +describe('getTuiAgentRestSignal', () => { + it('declares a rest signal for every launchable agent', () => { + const derived = Object.fromEntries( + Object.keys(TUI_AGENT_CONFIG) + .filter(isTuiAgent) + .map((agent) => [agent, getTuiAgentRestSignal(agent)]) + ) + expect(derived).toEqual(EXPECTED_REST_SIGNALS) + }) +}) diff --git a/src/shared/tui-agent-rest-signal.ts b/src/shared/tui-agent-rest-signal.ts new file mode 100644 index 00000000000..93f694a5d79 --- /dev/null +++ b/src/shared/tui-agent-rest-signal.ts @@ -0,0 +1,41 @@ +import { detectAgentStatusFromTitle } from './agent-title-status' +import { getSyntheticAgentTerminalTitle } from './synthetic-agent-title' +import type { TuiAgent } from './tui-agent' +import { TUI_AGENT_CONFIG } from './tui-agent-config' + +/** + * The strongest lane tui-idle-evidence.ts trusts to say an agent's TUI is at rest: + * - `hook-done`: its own hook reports the lead turn ended (DSH). + * - `synthetic-title`: its hooks drive an Orca-written ` ready` title. + * - `title`: its running process names itself in a title the status classifier reads. + * - `ready-body`: a ready screen Orca recognises (Muse; Qoder, whose idle title can show while + * its trust menu still owns input). + * - `none`: no stronger lane, so a quiet foreground process is its only one. Hooks or a first-party + * `done` (amp, kimi, command-code, freebuff) do not count: tui-idle takes their `working` as a + * veto, and trusts a first-party `done` only from DSH. + */ +export type TuiAgentRestSignal = 'hook-done' | 'synthetic-title' | 'title' | 'ready-body' | 'none' + +/** Lanes keyed on agent identity in tui-idle-evidence.ts, which no title table records. */ +const IDENTITY_REST_SIGNALS: Partial> = { + dsh: 'hook-done', + muse: 'ready-body', + qoder: 'ready-body' +} + +// Why derived, not declared per agent: the title tables are the evidence, so a second +// hand-kept copy could only drift from them. An agent with no evidence falls to `none`. +export function getTuiAgentRestSignal(agent: TuiAgent): TuiAgentRestSignal { + const identitySignal = IDENTITY_REST_SIGNALS[agent] + if (identitySignal) { + return identitySignal + } + if (getSyntheticAgentTerminalTitle(agent, 'done') !== null) { + return 'synthetic-title' + } + // Why the process name: the title tables match an agent by the name it runs as. + if (detectAgentStatusFromTitle(TUI_AGENT_CONFIG[agent].expectedProcess) !== null) { + return 'title' + } + return 'none' +} diff --git a/src/shared/tui-agent-selection.ts b/src/shared/tui-agent-selection.ts index 85567dcb334..acf305dad59 100644 --- a/src/shared/tui-agent-selection.ts +++ b/src/shared/tui-agent-selection.ts @@ -45,7 +45,8 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [ 'rovo', 'hermes', 'devin', - 'openclaw' + 'openclaw', + 'codebuddy' ] as const satisfies readonly TuiAgent[] // Why: fresh installs should expose Claude Agent Teams in agent pickers; the diff --git a/src/shared/tui-agent.ts b/src/shared/tui-agent.ts index 19722220888..a98c45101fd 100644 --- a/src/shared/tui-agent.ts +++ b/src/shared/tui-agent.ts @@ -3,6 +3,7 @@ export type TuiAgent = | 'claude' // Claude Code | 'claude-agent-teams' // Claude Code Agent Teams via Orca native panes + | 'codebuddy' // CodeBuddy Code | 'openclaude' // OpenClaude | 'codex' // OpenAI Codex | 'autohand' // Autohand Code CLI diff --git a/src/shared/worker-transcript-background-task-text.test.ts b/src/shared/worker-transcript-background-task-text.test.ts index 1e9d727406b..a612dbe0d8c 100644 --- a/src/shared/worker-transcript-background-task-text.test.ts +++ b/src/shared/worker-transcript-background-task-text.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from 'vitest' import type { NativeChatMessage } from './native-chat-types' -import { formatWorkerTranscriptMessage } from './worker-transcript-text' +import { formatWorkerTranscriptMessages } from './worker-transcript-text' + +const formatWorkerTranscriptMessage = (message: NativeChatMessage): string => + formatWorkerTranscriptMessages([message]).join('') function message(blocks: NativeChatMessage['blocks']): NativeChatMessage { return { id: 'm-1', role: 'system', blocks, timestamp: null, source: 'hook' } diff --git a/src/shared/worker-transcript-subagent-attribution.test.ts b/src/shared/worker-transcript-subagent-attribution.test.ts new file mode 100644 index 00000000000..f67a12d6702 --- /dev/null +++ b/src/shared/worker-transcript-subagent-attribution.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import type { NativeChatMessage } from './native-chat-types' +import { formatWorkerTranscriptMessages } from './worker-transcript-text' + +function message( + id: string, + text: string, + overrides: Partial = {} +): NativeChatMessage { + return { + id, + role: 'assistant', + blocks: [{ type: 'text', text }], + timestamp: 0, + source: 'transcript', + ...overrides + } +} + +const roster = message('roster', 'Kicked off 1 subagent', { + role: 'system', + blocks: [ + { type: 'text', text: 'Kicked off 1 subagent' }, + { + type: 'subagent-group', + groupId: 'group-1', + agents: [{ id: 'task-1', label: 'review the PR', state: 'working' }] + } + ] +}) + +describe("a worker's transcript never gives a subagent's words to the worker", () => { + it("tags a subagent's line with the subagent the roster names", () => { + const [own, , child] = formatWorkerTranscriptMessages([ + message('own', 'Delegating the review.'), + roster, + message('child', 'The PR is CLEAN.', { agentId: 'task-1' }) + ]) + expect(own).toBe('[assistant] Delegating the review.') + expect(child).toBe('[assistant, subagent review the PR] The PR is CLEAN.') + }) + + it('still tags a subagent that no roster on the page names', () => { + const [child] = formatWorkerTranscriptMessages([ + message('child', 'Still looking.', { agentId: 'task-9' }) + ]) + expect(child).toBe('[assistant, subagent] Still looking.') + }) +}) diff --git a/src/shared/worker-transcript-text-scaling.test.ts b/src/shared/worker-transcript-text-scaling.test.ts index 92283cb2431..d00778e4d1a 100644 --- a/src/shared/worker-transcript-text-scaling.test.ts +++ b/src/shared/worker-transcript-text-scaling.test.ts @@ -1,7 +1,10 @@ import { expect, it, vi } from 'vitest' -import { formatWorkerTranscriptMessage } from './worker-transcript-text' +import { formatWorkerTranscriptMessages } from './worker-transcript-text' import type { NativeChatMessage } from './native-chat-types' +const formatWorkerTranscriptMessage = (message: NativeChatMessage): string => + formatWorkerTranscriptMessages([message]).join('') + it('does not shift the remaining twin list for each matching roster', () => { const blocks: NativeChatMessage['blocks'] = Array.from({ length: 1000 }, (_, index) => ({ type: 'subagent-group', diff --git a/src/shared/worker-transcript-text.ts b/src/shared/worker-transcript-text.ts index 45a98a87835..fd42193edd8 100644 --- a/src/shared/worker-transcript-text.ts +++ b/src/shared/worker-transcript-text.ts @@ -12,8 +12,36 @@ import { subagentGroupFallbackText } from './native-chat-subagent-summary' import type { NativeChatMessage } from './native-chat-types' +import { agentJournalItemSubagentId } from './agent-session-journal-producer' +import { + nativeChatSubagentLabel, + nativeChatSubagentLabels +} from './native-chat-subagent-attribution' -export function formatWorkerTranscriptMessage(message: NativeChatMessage): string { +/** Every message in one read, each tagged with who it speaks as. A subagent is + * named by the roster on the same page. */ +export function formatWorkerTranscriptMessages(messages: readonly NativeChatMessage[]): string[] { + const subagentLabels = nativeChatSubagentLabels(messages) + return messages.map((message) => formatWorkerTranscriptMessage(message, subagentLabels)) +} + +/** Who a line speaks as. A subagent's lines are tagged as its own, so a peer reading + * a worker's transcript never takes a subagent's words for the worker's. */ +function workerTranscriptSpeaker( + message: NativeChatMessage, + subagentLabels: ReadonlyMap | undefined +): string { + if (agentJournalItemSubagentId(message) === null) { + return message.role + } + const label = nativeChatSubagentLabel(subagentLabels, message) + return label === undefined ? `${message.role}, subagent` : `${message.role}, subagent ${label}` +} + +function formatWorkerTranscriptMessage( + message: NativeChatMessage, + subagentLabels: ReadonlyMap +): string { // Every roster block is written beside a plain-text twin carrying the same // sentence, for clients that cannot draw the block. Text surfaces are those // clients, so they print the twin and drop the block. The renderer reaches the @@ -49,7 +77,7 @@ export function formatWorkerTranscriptMessage(message: NativeChatMessage): strin // than reading fields off a shape that has none. return '[unsupported block]' }) - return `[${message.role}] ${blocks.filter((line) => line !== null).join('\n')}`.trimEnd() + return `[${workerTranscriptSpeaker(message, subagentLabels)}] ${blocks.filter((line) => line !== null).join('\n')}`.trimEnd() } /** For each roster block, the sentence it must print itself — absent when a twin diff --git a/tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts b/tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts index cb11acf25f3..99b6b939f6d 100644 --- a/tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts +++ b/tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts @@ -299,6 +299,7 @@ test.describe('Combined diff invalidation freeze repro (STA-3420)', () => { ) console.log(`external-change burst measurement ${JSON.stringify(measurement)}`) + expect(measurement.sectionRowCount).toBe(1) expect(measurement.stuckLoadingRowCount).toBe(0) expect(measurement.editorCount).toBeGreaterThan(0) // Why: before the fix this window blocked continuously — p95 3963ms, 16 samples in 23s. diff --git a/tests/e2e/combined-diff-scroll-restore.spec.ts b/tests/e2e/combined-diff-scroll-restore.spec.ts index 496bead3db6..da02f8d0bdd 100644 --- a/tests/e2e/combined-diff-scroll-restore.spec.ts +++ b/tests/e2e/combined-diff-scroll-restore.spec.ts @@ -370,26 +370,27 @@ async function clickVisibleDiffLine(page: Page): Promise { return null } const containerRect = container.getBoundingClientRect() - const visibleLine = Array.from( - container.querySelectorAll('.monaco-diff-editor .view-line') - ).find((line) => { + for (const line of container.querySelectorAll( + '.monaco-diff-editor .view-line' + )) { const rect = line.getBoundingClientRect() - return ( - rect.height > 0 && - rect.bottom > containerRect.top && - rect.top < containerRect.bottom && - rect.right > containerRect.left && - rect.left < containerRect.right - ) - }) - if (!visibleLine) { - return null - } - const rect = visibleLine.getBoundingClientRect() - return { - x: rect.left + Math.min(12, Math.max(1, rect.width / 2)), - y: rect.top + rect.height / 2 + const left = Math.max(rect.left, containerRect.left) + const right = Math.min(rect.right, containerRect.right) + const top = Math.max(rect.top, containerRect.top) + const bottom = Math.min(rect.bottom, containerRect.bottom) + if (left >= right || top >= bottom) { + continue + } + const point = { + x: left + Math.min(12, (right - left) / 2), + y: (top + bottom) / 2 + } + // Sticky headers can cover a line whose rectangle intersects the viewport. + if (line.contains(document.elementFromPoint(point.x, point.y))) { + return point + } } + return null }) return linePoint !== null }, diff --git a/tests/e2e/cross-version-wire/agent-session-death-evidence-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/agent-session-death-evidence-downgrade.unit.test.ts new file mode 100644 index 00000000000..96780a18517 --- /dev/null +++ b/tests/e2e/cross-version-wire/agent-session-death-evidence-downgrade.unit.test.ts @@ -0,0 +1,61 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { expect, test } from 'vitest' +import { + agentSessionLeaseFixture, + agentSessionRecordFixture +} from '../../../src/shared/agent-session-record.test-fixture' +import { AgentSessionRecordStore } from '../../../src/main/runtime/agent-session-record-store' +import { agentSessionStorePath } from '../../../src/main/runtime/agent-session-record-store-file' +import { importReleaseCheckoutModule, materializeReleaseCheckout } from './release-checkout' + +// The last release before a proof of death named its owner and its last proof of life. +const BASELINE_REF = 'v1.4.211' +const SESSION = 'session-alpha-1' + +test('an older build loads a proof of death that names its owner and its last proof of life', async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-death-evidence-downgrade-')) + try { + // A live owner at fence 7, last renewed at 30000, found gone when this build relaunches. + writeFileSync( + agentSessionStorePath(directory), + JSON.stringify({ + schemaVersion: 2, + hostId: 'local', + records: { [SESSION]: agentSessionRecordFixture(agentSessionLeaseFixture()) }, + operations: {}, + retiredClaimKeys: [], + unusableRecords: {} + }) + ) + const store = await AgentSessionRecordStore.open({ directory, hostId: 'local' }) + await store.reconcileOnRestart({ probe: async () => ({ outcome: 'pid-absent' }), now: 90_000 }) + const evidence = store.getRecord(SESSION)?.lease.deathEvidence + expect(evidence).toEqual({ + kind: 'pid-absent', + detail: 'recorded pid absent on host', + observedAt: 90_000, + ownerFence: 7, + lastProvenAliveAt: 30_000 + }) + + const checkout = await materializeReleaseCheckout(BASELINE_REF) + const baseline = await importReleaseCheckoutModule( + checkout, + 'src/main/runtime/agent-session-record-store.ts' + ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pinned release exports this class with the open/read members called below; a missing one fails the test. + const OldStore = baseline.AgentSessionRecordStore as { + open: (args: { directory: string; hostId: string }) => Promise + } + const old = await OldStore.open({ directory, hostId: 'local' }) + expect(old.isSessionUnreadable(SESSION)).toBe(false) + expect(old.getRecord(SESSION)?.lease).toMatchObject({ + claimStatus: 'released', + deathEvidence: evidence + }) + } finally { + rmSync(directory, { recursive: true, force: true }) + } +}) diff --git a/tests/e2e/host-parked-pane-remote-viewer.spec.ts b/tests/e2e/host-parked-pane-remote-viewer.spec.ts index 6846369f9de..f2f8dd7fb39 100644 --- a/tests/e2e/host-parked-pane-remote-viewer.spec.ts +++ b/tests/e2e/host-parked-pane-remote-viewer.spec.ts @@ -45,7 +45,7 @@ import { launchPairedElectronClient } from './helpers/paired-electron-client' import { focusActiveTerminalInput } from './helpers/terminal' -import { waitForTabParked } from './helpers/terminal-hidden-parking' +import { parkHiddenTabBehindDecoy } from './helpers/terminal-hidden-parking' const PARK_DELAY_MS = 30_000 const PAINT_BUDGET_MS = 20_000 @@ -269,19 +269,6 @@ test('a cold-parked host pane keeps serving its paired remote viewer', async ({ // 3. The host cold-parks the pane the client is watching. Nobody touches // it on the host again. - // Two decoy tabs on the host: one to age the target, one to take the - // most-recently-hidden exemption. Sampled before and after the park lands - // so decoy churn cannot be mistaken for the park itself. - for (let i = 0; i < 2; i += 1) { - await orcaPage.evaluate((id) => { - const state = window.__store?.getState() - const tab = state?.createTab(id, undefined, undefined, { activate: true }) - if (tab) { - state?.setActiveTab(tab.id) - state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null) - } - }, worktreeId) - } const readClientState = async (): Promise => client.page.evaluate((id) => { const manager = window.__paneManagers?.get(id) @@ -292,8 +279,10 @@ test('a cold-parked host pane keeps serving its paired remote viewer', async ({ recoveryState: pane?.container?.dataset?.ptyRecoveryState ?? null } }, webTabId) - console.log(`[sta2854] decoys-created client=${JSON.stringify(await readClientState())}`) - await waitForTabParked(orcaPage, hostTabId, { parkDelayMs: PARK_DELAY_MS }) + console.log(`[sta2854] before-park client=${JSON.stringify(await readClientState())}`) + await parkHiddenTabBehindDecoy(orcaPage, worktreeId, hostTabId, { + parkDelayMs: PARK_DELAY_MS + }) console.log(`[sta2854] post-park client=${JSON.stringify(await readClientState())}`) // Direct probe: is the host-minted terminal handle still resolvable once diff --git a/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts b/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts index fe045e3814a..7c413c06126 100644 --- a/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts +++ b/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts @@ -286,8 +286,16 @@ async function probeInteractivity( `LINE:${token}`, LIVE_PAINT_BUDGET_MS ) - const paneGrid = await readActivePaneGrid(page, target.webTabId) + let paneGrid = await readActivePaneGrid(page, target.webTabId) + let ptyGrid = readPtyGridFromContent(readSink(target.sinkPath)) + console.log(`[paired-grid-initial] ${JSON.stringify({ name, paneGrid, ptyGrid })}`) const diagnostics = await readPaneDiagnostics(page, worktreeId, target.webTabId) + const screenshotPath = test.info().outputPath(`paired-terminal-${name}.png`) + await page.screenshot({ path: screenshotPath }) + await test.info().attach(`paired-terminal-${name}`, { + path: screenshotPath, + contentType: 'image/png' + }) let paintedAfterFlip = paintedLive if (!paintedLive) { await openClientTab(page, worktreeId, flipTo.webTabId) @@ -299,6 +307,31 @@ async function probeInteractivity( LIVE_PAINT_BUDGET_MS ) } + // Preserve the scenario report even when geometry fails to converge. + await expect + .configure({ soft: true }) + .poll( + async () => { + paneGrid = await readActivePaneGrid(page, target.webTabId) + ptyGrid = readPtyGridFromContent(readSink(target.sinkPath)) + return { + paneGrid, + ptyGrid, + converged: + paneGrid !== null && + ptyGrid !== null && + paneGrid.cols > 0 && + paneGrid.rows > 0 && + paneGrid.cols === ptyGrid.cols && + paneGrid.rows === ptyGrid.rows + } + }, + { + timeout: REVEAL_BUDGET_MS, + message: `${name}: revealed pane and host PTY grids did not converge` + } + ) + .toMatchObject({ converged: true }) const sink = readSink(target.sinkPath) return { name, @@ -307,7 +340,7 @@ async function probeInteractivity( paintedLive, paintedAfterFlip, paneGrid, - ptyGrid: readPtyGridFromContent(sink), + ptyGrid, diagnostics } } diff --git a/tests/e2e/terminal-tab-close-restart-persistence.spec.ts b/tests/e2e/terminal-tab-close-restart-persistence.spec.ts index 9df296d287b..20421dd1730 100644 --- a/tests/e2e/terminal-tab-close-restart-persistence.spec.ts +++ b/tests/e2e/terminal-tab-close-restart-persistence.spec.ts @@ -137,8 +137,19 @@ test('durable whole-tab close removes a split tab across restart', async (// oxl const secondLaunch = await session.launch() secondApp = secondLaunch.app await waitForSessionReady(secondLaunch.page) - const restoredWorktreeId = await attachRepoAndOpenTerminal(secondLaunch.page, repoPath) - expect(restoredWorktreeId).toBe(worktreeId) + // Activation deliberately creates a fresh terminal; observe passive hydration first. + await expect + .poll(() => + secondLaunch.page.evaluate( + (id) => + window.__store + ?.getState() + .allWorktrees() + .some((worktree) => worktree.id === id), + worktreeId + ) + ) + .toBe(true) // Why: wait past initial worktree effects so this checks resurrection, not // only the first hydrated frame before default-tab logic has run. diff --git a/tests/tools/google-signin-ua-probe.cjs b/tests/tools/google-signin-ua-probe.cjs index 47d8b419d7a..2b3e9638720 100644 --- a/tests/tools/google-signin-ua-probe.cjs +++ b/tests/tools/google-signin-ua-probe.cjs @@ -241,7 +241,7 @@ app.whenReady().then(async () => { return } if (isAppMode) { - // Mirror applyGoogleAuthUserAgent: Firefox UA on auth navs, restore the + // Mirror presentTabIdentityAtNavigationStart: Firefox UA on auth navs, restore the // cleaned session UA otherwise. This WebContents UA is what leaks onto // cross-host subresource requests while the auth document is on screen. const current = window.webContents.getUserAgent()